Python多線程雞年不雞肋

當初在剛學習python多線程時,上網搜索資料幾乎都是一片倒的反應python沒有真正意義上的多線程,python多線程就是雞肋。當時不明因此,只是瞭解到python帶有GIL解釋器鎖的概念,同一時刻只能有一個線程在運行,遇到IO操做纔會釋放切換。那麼,python多線程是否真的很雞肋呢?要解決這個疑惑,我想必須親自動手測試。

通過對比python與java的多線程測試,我發現python多線程的效率確實不如java,但遠尚未達到雞肋的程度,那麼跟其餘機制相比較呢?java

觀點:用多進程替代多線程需求

展轉了多篇博文,我看到了一些網友的觀點,以爲應該使用python多進程來代替多線程的需求,由於多進程不受GIL的限制。因而我便動手使用多進程去解決一些併發問題,期間也遇到了一些坑,所幸大部分查找資料解決了,而後對多進程作了簡單彙總介紹Python多進程
那麼是否多進程能徹底替代多線程呢?別急,咱們繼續往下看。python

觀點:協程爲最佳方案

協程的概念目前來講是比較火熱的,協程不一樣於線程的地方在於協程不是操做系統進行切換,而是由程序員編碼進行切換的,也就是說切換是由程序員控制的,這樣就沒有了線程所謂的安全問題。協程的概念很是廣而深,本文暫不作具體介紹,之後會單獨成文。程序員

測試數據

好了,網上的觀點無非是使用多進程或者協程來代替多線程(固然換編程語言,換解釋器之類方法除外),那麼咱們就來測試下這三者的性能之差。既然要公平測試,就應該考慮IO密集型與CPU密集型的問題,因此分兩組數據進行測試。編程

IO密集型測試

測試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測試結果:

  • 併發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()函數建立線程。
代碼:

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方法,能夠啓動子線程。

  • 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類,建立線程。
代碼:

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多進程現場,其餘關於多線程問題,能夠下方留言討 論

相關文章
相關標籤/搜索