首先,說明一下多線程的應用場景:當python處理多個任務時,這些任務本質是非同步的,需要有多個併發事務,各個事務的運行順序可以是不確定的、隨機的、不可預測的。計算密集型的任務可以順序執行分隔成的多個子任務,也可以用多線程的方式處理。但I/O密集型的任務就不好以單線程方式處理了,如果不用多線程,只能用 ...
首先,說明一下多線程的應用場景:當python處理多個任務時,這些任務本質是非同步的,需要有多個併發事務,各個事務的運行順序可以是不確定的、隨機的、不可預測的。計算密集型的任務可以順序執行分隔成的多個子任務,也可以用多線程的方式處理。但I/O密集型的任務就不好以單線程方式處理了,如果不用多線程,只能用一個或多個計時器來處理實現。
下麵說一下進程與線程:進程(有時叫重量級進程),是程式的一次執行,正如我們在centos中,ps -aux | grep something 的時候,總有一個他自身產生的進程,也就是這個grep進程,每個進程有自己的地址空間、記憶體、數據棧、及其他記錄其運行軌跡的輔助數據,因此各個進程也不能直接共用信息,只能用進程間通信(IPC)。
線程(輕量級進程),與進程最大的區別是,所有的線程運行在同一個進程中,共用相同的運行環境,共用同一片數據空間。所以線程之間可以比進程之間更方便的共用數據以及相互通訊,併發執行完成事務。
為了方便理解記憶進程與線程的關係,我們可以做一個類比:把cpu比作一個搬家公司,而這家搬家公司只有一輛車(進程)來供使用,開始,這家搬家公司很窮,只有一個員工(單線程),那麼,這個搬家公司一天,最多只能搬5家,後來,老闆賺到錢了,他沒買車,而是多雇了n個員工(多線程),這樣,每個員工會被安排每次只搬一家,然後就去休息,把車讓出來,讓其他人搬下一家,這看起來其實並沒有提高多少效率,反而增加了成本是吧,這是因為GIL(Global Interpreter Lock) 全局解釋器鎖,保證了線程安全(保證數據被安全讀取),即同時只能有一個線程在CPU上運行,這是python特有的機制,也就是說,即使你的運行環境具有雙CPU,python虛擬機也只會使用一個cpu,也就是說GIL 直接導致 CPython 不能利用物理多核的性能加速運算。具體的詳細解釋(歷史遺留問題,硬體發展太快)可以參考這篇博客:
http://blog.sina.com.cn/s/blog_64ecfc2f0102uzzf.html
在python核心編程中,作者極力建議我們不要使用thread模塊,而是要使用threading模塊,原因如下:
1、當主線程退出時,所有其他線程沒有被清除就退出了,thread模塊無法保護所有子線程的安全退出。即,thread 模塊不支持守護進程。
2、thread模塊的屬性有可能會與threading出現衝突。
3、低級別的thread模塊的同步原語很少(實際只有一個,應該是sleep)。
一、thread模塊
以下是不使用GIL和使用GIL的兩個示例代碼:
1.不使用GIL的代碼示例:
1 from time import sleep,ctime
2 import thread
3
4 def loop0():
5 print 'start loop 0 at: ',ctime()
6 sleep(4)
7 print 'loop 0 done at: ',ctime()
8 def loop1():
9 print 'start loop 1 at: ',ctime()
10 sleep(2)
11 print 'loop 1 done at: ',ctime()
12 def main():
13 print 'start at: ',ctime()
14 thread.start_new_thread(loop0,())
15 thread.start_new_thread(loop1,())
16 sleep(6)
17 print 'all loop is done, ' ,ctime()
18
19 if __name__=='__main__':
20 main()
21
22
23 輸出結果:
24
25 start at: Thu Jan 28 10:46:27 2016
26 start loop 0 at: Thu Jan 28 10:46:27 2016
27
28 start loop 1 at: Thu Jan 28 10:46:27 2016
29 loop 1 done at: Thu Jan 28 10:46:29 2016
30 loop 0 done at: Thu Jan 28 10:46:31 2016
31 all loop is done, Thu Jan 28 10:46:33 2016
由以上輸出可以看出,我們成功開啟了兩個線程,並且與主線程同步,在第2s時,loop1先完成,第4s時loop0完成,又過了2s,主線程完成結束。整個主線程經過了6s,loop0和loop1同步完成。
2、使用GIL的代碼示例:
1 import thread
2 from time import sleep,ctime
3 loops = [4,2]
4 def loop(nloop,nsec,lock):
5 print 'start loop',nloop,'at: ',ctime()
6 sleep(nsec)
7 print 'loop',nloop,'done at:',ctime()
8 lock.release()
9 def main():
10 print 'starting at:',ctime()
11 locks = []
12 nloops = range(len(loops))
13
14 for i in nloops:
15 lock = thread.allocate_lock() #創建鎖的列表,存在locks中
16 lock.acquire()
17 locks.append(lock)
18 for i in nloops:
19 thread.start_new_thread(loop,(i,loops[i],locks[i])) #創建線程,參數為迴圈號,睡眠時間,鎖
20 for i in nloops:
21 while locks[i].locked(): #等待迴圈完成,解鎖
22 pass
23 print 'all DONE at:',ctime()
24 if __name__ == '__main__':
25 main()
26
27
28 以上輸出如下:
29
30 starting at: Thu Jan 28 14:59:22 2016
31 start loop 0 at: Thu Jan 28 14:59:22 2016
32
33 start loop 1 at: Thu Jan 28 14:59:22 2016
34 loop 1 done at: Thu Jan 28 14:59:24 2016
35 loop 0 done at: Thu Jan 28 14:59:26 2016
36 all DONE at: Thu Jan 28 14:59:26 2016
歷時4秒,這樣效率得到提高,也比在主線程中用一個sleep()函數來計時更為合理。
二、threading模塊
1、Thread類
在thread類中,可以用以下三種方法來創建線程:
(1)創建一個thread實例,傳給它一個函數
(2)創建一個thread實例,傳給它一個可調用的類對象
(3)從thread派生出一個子類,創建這個子類的對象
方法(1)
1 __author__ = 'dell'
2 import threading
3 from time import sleep,ctime
4 def loop0():
5 print 'start loop 0 at:',ctime()
6 sleep(4)
7 print 'loop 0 done at:',ctime()
8 def loop1():
9 print 'start loop 1 at:',ctime()
10 sleep(2)
11 print 'loop 1 done at:',ctime()
12 def main():
13 print 'starting at:',ctime()
14 threads = []
15 t1 = threading.Thread(target=loop0,args=()) #創建線程
16 threads.append(t1)
17 t2 = threading.Thread(target=loop1,args=())
18 threads.append(t2)
19 for t in threads:
20 t.setDaemon(True)<span style="white-space:pre"> </span> #開啟守護線程(一定要在start()前調用)
21 t.start()<span style="white-space:pre"> </span> #開始線程執行
22 for t in threads:<span style="white-space:pre"> </span>
23 t.join()<span style="white-space:pre"> </span> #將程式掛起阻塞,直到線程結束,如果給出數值,則最多阻塞timeout秒
24
25 if __name__ == '__main__':
26 main()
27 print 'All DONE at:',ctime()
28
29 在這裡,就不用像thread模塊那樣要管理那麼多鎖(分配、獲取、釋放、檢查等)了,同時我也減少了迴圈的代碼,直接自己編號迴圈了,得到輸出如下:
30
31
32 starting at: Thu Jan 28 16:38:14 2016
33 start loop 0 at: Thu Jan 28 16:38:14 2016
34 start loop 1 at: Thu Jan 28 16:38:14 2016
35 loop 1 done at: Thu Jan 28 16:38:16 2016
36 loop 0 done at: Thu Jan 28 16:38:18 2016
37 All DONE at: Thu Jan 28 16:38:18 2016
結果相同,但是從代碼的邏輯來看,要清晰的多了。其他兩種在此就不貼出代碼了。實例化一個Thread與調用thread.start_new_thread直接最大的區別就是新的線程不會立即開始執行,也就是說,在threading模塊的Thread類中當我們實例化之後,再調用.start()函數後被統一執行,這使得我們的程式具有很好的同步特性。
下麵是單線程與多線程的一個對比示例,分別以乘除完成兩組運算,從而看出多線程對效率的提高
1 from time import ctime,sleep
2 import threading
3
4 def multi():
5 num1 = 1
6 print 'start mutiple at:',ctime()
7 for i in range(1,10):
8 num1 = i*num1
9 sleep(0.2)
10 print 'mutiple finished at:',ctime()
11 return num1
12 def divide():
13 num2 = 100
14 print 'start division at:',ctime()
15 for i in range(1,10):
16 num2 = num2/i
17 sleep(0.4)
18 print 'division finished at:',ctime()
19 return num2
20 def main():
21 print '---->single Thread'
22 x1 = multi()
23 x2 = divide()
24 print 'The sum is ',sum([x1,x2]),'\nfinished singe thread',ctime()
25
26 print '----->Multi Thread'
27 threads = []
28 t1 = threading.Thread(target=multi,args=())
29 threads.append(t1)
30 t2 = threading.Thread(target=divide,args=())
31 threads.append(t2)
32 for t in threads:
33 t.setDaemon(True)
34 t.start()
35 for t in threads:
36 t.join()
37
38 if __name__ == '__main__':
39 main()
40
41 結果如下:
42
43
44
45 ---->single Thread
46
47 start mutiple at: Thu Jan 28 21:41:18 2016
48
49 mutiple finished at: Thu Jan 28 21:41:20 2016
50
51 start division at: Thu Jan 28 21:41:20 2016
52
53 division finished at: Thu Jan 28 21:41:24 2016
54
55 The sum is 362880
56
57 finished singe thread Thu Jan 28 21:41:24 2016
58
59 ----->Multi Thread
60
61 start mutiple at: Thu Jan 28 21:41:24 2016
62
63 start division at: Thu Jan 28 21:41:24 2016
64
65 mutiple finished at: Thu Jan 28 21:41:26 2016
66
67 division finished at: Thu Jan 28 21:41:27 2016
68
69 The sum is : 362880