術業有專攻,如是而已
當初在剛學習python多執行緒時,上網搜尋資料幾乎都是一片倒的反應python沒有真正意義上的多執行緒,python多執行緒就是雞肋。當時不明所以,只是瞭解到python帶有GIL直譯器鎖的概念,同一時刻只能有一個執行緒在執行,遇到IO操作才會釋放切換。那麼,python多執行緒是否真的很雞肋呢?要解決這個疑惑,我想必須親自動手測試。
經過對比python與java的多執行緒測試,我發現python多執行緒的效率確實不如java,但遠還沒有達到雞肋的程度,那麼跟其他機制相比較呢?
觀點:用多程式替代多執行緒需求
輾轉了多篇博文,我看到了一些網友的觀點,覺得應該使用python多程式來代替多執行緒的需求,因為多程式不受GIL的限制。於是我便動手使用多程式去解決一些併發問題,期間也遇到了一些坑,所幸大部分查詢資料解決了,然後對多程式做了簡單彙總介紹Python多程式。
那麼是否多程式能完全替代多執行緒呢?別急,我們繼續往下看。
觀點:協程為最佳方案
協程的概念目前來說是比較火熱的,協程不同於執行緒的地方在於協程不是作業系統進行切換,而是由程式設計師編碼進行切換的,也就是說切換是由程式設計師控制的,這樣就沒有了執行緒所謂的安全問題。協程的概念非常廣而深,本文暫不做具體介紹,以後會單獨成文。
測試資料
好了,網上的觀點無非是使用多程式或者協程來代替多執行緒(當然換程式語言,換直譯器之類方法除外),那麼我們就來測試下這三者的效能之差。既然要公平測試,就應該考慮IO密集型與CPU密集型的問題,所以分兩組資料進行測試。
IO密集型測試
測試IO密集型,我選擇最常用的爬蟲功能,計算爬蟲訪問bing所需要的時間。(主要測試多執行緒與協程,單執行緒與多程式就不測了,因為沒有必要)
測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#! -*- coding:utf-8 -*- from gevent import monkey;monkey.patch_all() import gevent import time import threading import urllib2 def urllib2_(url): try: urllib2.urlopen(url,timeout=10).read() except Exception,e: print e def gevent_(urls): jobs=[gevent.spawn(urllib2_,url) for url in urls] gevent.joinall(jobs,timeout=10) for i in jobs: i.join() def thread_(urls): a=[] for url in urls: t=threading.Thread(target=urllib2_,args=(url,)) a.append(t) for i in a: i.start() for i in a: i.join() if __name__=="__main__": urls=["https://www.bing.com/"]*10 t1=time.time() gevent_(urls) t2=time.time() print 'gevent-time:%s' % str(t2-t1) thread_(urls) t4=time.time() print 'thread-time:%s' % str(t4-t2) |
測試結果:
訪問10次
gevent-time:0.380326032639
thread-time:0.376606941223
訪問50次
gevent-time:1.3358900547
thread-time:1.59564089775
訪問100次
gevent-time:2.42984986305
thread-time:2.5669670105
訪問300次
gevent-time:6.66330099106
thread-time:10.7605059147
從結果可以看出,當併發數不斷增大時,協程的效率確實比多執行緒要高,但在併發數不是那麼高時,兩者差異不大。
CPU密集型
CPU密集型,我選擇科學計算的一些功能,計算所需時間。(主要測試單執行緒、多執行緒、協程、多程式)
測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#! -*- coding:utf-8 -*- from multiprocessing import Process as pro from multiprocessing.dummy import Process as thr from gevent import monkey;monkey.patch_all() import gevent def run(i): lists=range(i) list(set(lists)) if __name__=="__main__": ''' 多程式 ''' for i in range(30): ##10-2.1s 20-3.8s 30-5.9s t=pro(target=run,args=(5000000,)) t.start() ''' 多執行緒 ''' # for i in range(30): ##10-3.8s 20-7.6s 30-11.4s # t=thr(target=run,args=(5000000,)) # t.start() ''' 協程 ''' # jobs=[gevent.spawn(run,5000000) for i in range(30)] ##10-4.0s 20-7.7s 30-11.5s # gevent.joinall(jobs) # for i in jobs: # i.join() ''' 單執行緒 ''' # for i in range(30): ##10-3.5s 20-7.6s 30-11.3s # run(5000000) |
測試結果:
- 併發10次:【多程式】2.1s 【多執行緒】3.8s 【協程】4.0s 【單執行緒】3.5s
- 併發20次:【多程式】3.8s 【多執行緒】7.6s 【協程】7.7s 【單執行緒】7.6s
- 併發30次:【多程式】5.9s 【多執行緒】11.4s 【協程】11.5s 【單執行緒】11.3s
可以看到,在CPU密集型的測試下,多程式效果明顯比其他的好,多執行緒、協程與單執行緒效果差不多。這是因為只有多程式完全使用了CPU的計算能力。在程式碼執行時,我們也能夠看到,只有多程式可以將CPU使用率佔滿。
本文結論
從兩組資料我們不難發現,python多執行緒並沒有那麼雞肋。如若不然,Python3為何不去除GIL呢?對於此問題,Python社群也有兩派意見,這裡不再論述,我們應該尊重Python之父的決定。
至於何時該用多執行緒,何時用多程式,何時用協程?想必答案已經很明顯了。
當我們需要編寫併發爬蟲等IO密集型的程式時,應該選用多執行緒或者協程(親測差距不是特別明顯);當我們需要科學計算,設計CPU密集型程式,應該選用多程式。當然以上結論的前提是,不做分散式,只在一臺伺服器上測試。
答案已經給出,本文是否就此收尾?既然已經論述Python多執行緒尚有用武之地,那麼就來介紹介紹其用法吧。
Multiprocessing.dummy模組
Multiprocessing.dummy用法與多程式Multiprocessing用法類似,只是在import包的時候,加上.dummy。
用法參考Multiprocessing用法
threading模組
這是python自帶的threading多執行緒模組,其建立多執行緒主要有2種方式。一種為繼承threading類,另一種使用threading.Thread函式,接下來將會分別介紹這兩種用法。
Usage【1】
利用threading.Thread()函式建立執行緒。
程式碼:
1 2 3 4 5 |
def run(i): print i for i in range(10): t=threading.Thread(target=run,args=(i,)) t.start() |
說明:Thread()函式有2個引數,一個是target,內容為子執行緒要執行的函式名稱;另一個是args,內容為需要傳遞的引數。建立完子執行緒,將會返回一個物件,呼叫物件的start方法,可以啟動子執行緒。
執行緒物件的方法:
- Start() 開始執行緒的執行
- Run() 定義執行緒的功能的函式
- Join(timeout=None) 程式掛起,直到執行緒結束;如果給了timeout,則最多阻塞timeout秒
- getName() 返回執行緒的名字
- setName() 設定執行緒的名字
- isAlive() 布林標誌,表示這個執行緒是否還在執行
- isDaemon() 返回執行緒的daemon標誌
- setDaemon(daemonic) 把執行緒的daemon標誌設為daemonic(一定要在start()函式前呼叫)
- t.setDaemon(True) 把父執行緒設定為守護執行緒,當父程式結束時,子程式也結束。
threading類的方法:
- threading.enumerate() 正在執行的執行緒數量
Usage【2】
通過繼承threading類,建立執行緒。
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import threading class test(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): try: print "code one" except: pass for i in range(10): cur=test() cur.start() for i in range(10): cur.join() |
說明:此方法繼承了threading類,並且重構了run函式功能。
獲取執行緒返回值問題
有時候,我們往往需要獲取每個子執行緒的返回值。然而通過呼叫普通函式,獲取return值的方式在多執行緒中並不適用。因此需要一種新的方式去獲取子執行緒返回值。
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import threading class test(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.tag=1 def get_result(self): if self.tag==1: return True else: return False f=test() f.start() while f.isAlive(): continue print f.get_result() |
說明:多執行緒獲取返回值的首要問題,就是子執行緒什麼時候結束?我們應該什麼時候去獲取返回值?可以使用isAlive()方法判斷子執行緒是否存活。
控制執行緒執行數目
當需要執行的任務非常多時,我們往往需要控制執行緒的數量,threading類自帶有控制執行緒數量的方法。
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import threading maxs=10 ##併發的執行緒數量 threadLimiter=threading.BoundedSemaphore(maxs) class test(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): threadLimiter.acquire() #獲取 try: print "code one" except: pass finally: threadLimiter.release() #釋放 for i in range(100): cur=test() cur.start() for i in range(100): cur.join() |
說明:以上程式可以控制多執行緒併發數為10,超過這個數量會引發異常。
除了自帶的方法,我們還可以設計其他方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
threads=[] ''' 建立所有執行緒 ''' for i in range(10): t=threading.Thread(target=run,args=(i,)) threads.append(t) ''' 啟動列表中的執行緒 ''' for t in threads: t.start() while True: #判斷正在執行的執行緒數量,如果小於5則退出while迴圈, #進入for迴圈啟動新的程式.否則就一直在while迴圈進入死迴圈 if(len(threading.enumerate())<5): break |
以上兩種方式皆可以,本人更喜歡用下面那種方式。
執行緒池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import threadpool def ThreadFun(arg1,arg2): pass def main(): device_list=[object1,object2,object3......,objectn]#需要處理的裝置個數 task_pool=threadpool.ThreadPool(8)#8是執行緒池中執行緒的個數 request_list=[]#存放任務列表 #首先構造任務列表 for device in device_list: request_list.append(threadpool.makeRequests(ThreadFun,[((device, ), {})])) #將每個任務放到執行緒池中,等待執行緒池中執行緒各自讀取任務,然後進行處理,使用了map函式,不瞭解的可以去了解一下。 map(task_pool.putRequest,request_list) #等待所有任務處理完成,則返回,如果沒有處理完,則一直阻塞 task_pool.poll() if __name__=="__main__": main() |
多程式問題,可以趕赴Python多程式現場,其他關於多執行緒問題,可以下方留言討論
申明:本文談不上原創,其中借鑑了網上很多大牛的文章,本人只是在此測試論述Python多執行緒相關問題,並簡單介紹Python多執行緒的基本用法,為新手朋友解惑。