當初在剛學習python多線程時,上網搜索資料幾乎都是一片倒的反應python沒有真正意義上的多線程,python多線程就是雞肋。當時不明因此,只是瞭解到python帶有GIL解釋器鎖的概念,同一時刻只能有一個線程在運行,遇到IO操做纔會釋放切換。那麼,python多線程是否真的很雞肋呢?要解決這個疑惑,我想必須親自動手測試。
通過對比python與java的多線程測試,我發現python多線程的效率確實不如java,但遠尚未達到雞肋的程度,那麼跟其餘機制相比較呢?java
展轉了多篇博文,我看到了一些網友的觀點,以爲應該使用python多進程來代替多線程的需求,由於多進程不受GIL的限制。因而我便動手使用多進程去解決一些併發問題,期間也遇到了一些坑,所幸大部分查找資料解決了,而後對多進程作了簡單彙總介紹Python多進程。
那麼是否多進程能徹底替代多線程呢?別急,咱們繼續往下看。python
協程的概念目前來講是比較火熱的,協程不一樣於線程的地方在於協程不是操做系統進行切換,而是由程序員編碼進行切換的,也就是說切換是由程序員控制的,這樣就沒有了線程所謂的安全問題。協程的概念很是廣而深,本文暫不作具體介紹,之後會單獨成文。程序員
好了,網上的觀點無非是使用多進程或者協程來代替多線程(固然換編程語言,換解釋器之類方法除外),那麼咱們就來測試下這三者的性能之差。既然要公平測試,就應該考慮IO密集型與CPU密集型的問題,因此分兩組數據進行測試。編程
測試IO密集型,我選擇最經常使用的爬蟲功能,計算爬蟲訪問bing所須要的時間。(主要測試多線程與協程,單線程與多進程就不測了,由於沒有必要)
測試代碼:安全
Python服務器
#! -*- 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)
CPU密集型測試結果:多線程
訪問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密集型,我選擇科學計算的一些功能,計算所需時間。(主要測試單線程、多線程、協程、多進程)
測試代碼:app
Python編程語言
#! -*- 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測試結果:
能夠看到,在CPU密集型的測試下,多進程效果明顯比其餘的好,多線程、協程與單線程效果差很少。這是由於只有多進程徹底使用了CPU的計算能力。在代碼運行時,咱們也可以看到,只有多進程能夠將CPU使用率佔滿。
從兩組數據咱們不難發現,python多線程並無那麼雞肋。如若否則,Python3爲什麼不去除GIL呢?對於此問題,Python社區也有兩派意見,這裏再也不論述,咱們應該尊重Python之父的決定。
至於什麼時候該用多線程,什麼時候用多進程,什麼時候用協程?想必答案已經很明顯了。
當咱們須要編寫併發爬蟲等IO密集型的程序時,應該選用多線程或者協程(親測差距不是特別明顯);當咱們須要科學計算,設計CPU密集型程序,應該選用多進程。固然以上結論的前提是,不作分佈式,只在一臺服務器上測試。
答案已經給出,本文是否就此收尾?既然已經論述Python多線程尚有用武之地,那麼就來介紹介紹其用法吧。
Multiprocessing.dummy用法與多進程Multiprocessing用法相似,只是在import包的時候,加上.dummy。
用法參考Multiprocessing用法
這是python自帶的threading多線程模塊,其建立多線程主要有2種方式。一種爲繼承threading類,另外一種使用threading.Thread函數,接下來將會分別介紹這兩種用法。
利用threading.Thread()函數建立線程。
代碼:
Python
def run(i): print i for i in range(10): t=threading.Thread(target=run,args=(i,)) t.start()
線程對象的方法:說明:Thread()函數有2個參數,一個是target,內容爲子線程要執行的函數名稱;另外一個是args,內容爲須要傳遞的參數。建立完子線程,將會返回一個對象,調用對象的start方法,能夠啓動子線程。
threading類的方法:
經過繼承threading類,建立線程。
代碼:
Python
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值的方式在多線程中並不適用。所以須要一種新的方式去獲取子線程返回值。
代碼:
Python
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類自帶有控制線程數量的方法。
代碼:
Python
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,超過這個數量會引起異常。
除了自帶的方法,咱們還能夠設計其餘方案:
Python
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
線程池以上兩種方式皆能夠,本人更喜歡用下面那種方式。
Python
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多進程現場,其餘關於多線程問題,能夠下方留言討 論