當前位置:
首頁 > 知識 > Python 多線程雞年不雞肋

Python 多線程雞年不雞肋


術業有專攻,如是而已





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



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




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




輾轉了多篇博文,我看到了一些網友的觀點,覺得應該使用python多進程來代替多線程的需求,因為多進程不受GIL的限制。於是我便動手使用多進程去解決一些並發問題,期間也遇到了一些坑,所幸大部分查找資料解決了,然後對多進程做了簡單匯總介紹Python多進程。




那麼是否多進程能完全替代多線程呢?別急,我們繼續往下看。



觀點:協程為最佳方案




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




測試數據




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



IO密集型測試




測試IO密集型,我選擇最常用的爬蟲功能,計算爬蟲訪問bing所需要的時間。(主要測試多線程與協程,單線程與多進程就不測了,因為沒有必要)


測試代碼:





#! -*- 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密集型,我選擇科學計算的一些功能,計算所需時間。(主要測試單線程、多線程、協程、多進程)




測試代碼:





#! -*- 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()函數創建線程。




代碼:





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類,創建線程。




代碼:





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值的方式在多線程中並不適用。因此需要一種新的方式去獲取子線程返回值。




代碼:





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類自帶有控制線程數量的方法。




代碼:





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,超過這個數量會引發異常。




除了自帶的方法,我們還可以設計其他方案:





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




以上兩種方式皆可以,本人更喜歡用下面那種方式。




線程池





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多進程(http://python.jobbole.com/87760/)現場,其他關於多線程問題,可以下方留言討論




申明:本文談不上原創,其中借鑒了網上很多大牛的文章,本人只是在此測試論述Python多線程相關問題,並簡單介紹Python多線程的基本用法,為新手朋友解惑。





  • 來源:nMask



  • thief.one/2017/02/17/Python多線程雞年不雞肋/



  • Python開發整理髮布,轉載請聯繫作者獲得授權


↙點擊「

閱讀原文

」,加入 


『程序員大咖』

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python 的精彩文章:

其他語言無法比擬的6個Python特性
Python中 else 塊那點事

TAG:Python |

您可能感興趣

python udp的應用 ,多線程實現聊天功能
Python爬蟲框架之pyspider
DIY 一個 micropython 的多功能無線電子鐘
PyQt5+ Python3 多線程通信
python筆記12-python多線程之事件
利用ngx_python模塊嵌入到Python腳本
Netflix幕後最大功臣是Python!
ChainMap:將多個字典視為一個,解鎖Python超能力。
為什麼編程啟蒙要學 Scratch,不是Python?
python logging 日誌模塊以及多進程日誌
從Scratch到Python
Python 編程 5 年後,我轉向了 Go!
python-django 項目部署,不難!
Netflix 背後的大功臣是 Python!
python爬取youtube視頻 多線程 非中文自動翻譯
Python 的 except 怪癖
python基礎學習第一課,如何兩步簡單安裝python
Python 之父拋棄 Python!
用於多元時間序列的 Python 模塊——Seglearn
寫 Python 時的 5 個壞習慣 | python3