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
:
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
()
"gevent-time:%s"
%
str
(
t2
-
t1
)
thread_
(
urls
)
t4
=
time
.
time
()
"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
)
:
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
:
"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
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
:
"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開發整理髮布,轉載請聯繫作者獲得授權
。
↙點擊「
閱讀原文
」,加入
『程序員大咖』


※其他語言無法比擬的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