python 程序、執行緒 (二)
一、多執行緒與多程序的對比
在python 程序、執行緒 (一)中簡單的說過,CPython中的GIL使得同一時刻只能有一個執行緒執行,即併發執行。並且即使是多核CPU,GIL使得同一個程序中的多個執行緒也無法對映到多個CPU上執行,這麼做最初是為了安全著想,慢慢的也成為了限制CPython效能的問題。
就像是一個執行緒想要執行,就必須得到GIL,否則就不能拿到CPU資源。但是也不是說一個執行緒在拿到CPU資源後就一勞永逸,在執行的過程中GIL可能會釋放並被其他執行緒獲取,所以說其它的執行緒會與本執行緒競爭CPU資源。
在understand GIL:http://www.dabeaz.com/python/UnderstandingGIL.pdf
多執行緒在python2中:當一個執行緒進行I/O的時候會釋放鎖,另外當ticks計數達到100(ticks可以看作是Python自身的一個計數器,也可對比著位元組碼指令理解,專門做用於GIL,每次釋放後歸零,這個計數可以通過 sys.setcheckinterval 來調整)。鎖釋放之後,就涉及到執行緒的排程,執行緒的鎖進行,執行緒的切換。這是會消耗CPU資源,因此會造成程式效能問題和等待時延。特別是在CPU密集型程式碼時。
但是對於多程序,GIL就無法限制,多個程序可以再多個CPU上執行,充分利用多核優勢。事情往往是相對的,雖然可以充分利用多核優勢,但是程序之間的切換卻比執行緒的切換代價更高。
所以選擇多執行緒還是多程序,主要還是看怎樣權衡代價,什麼樣的情況。
1、CPU密集程式碼
下面來利用斐波那契數列模擬CPU密集運算。
def fib(n):
# 求斐波那契數列的第n個值
if n<=2:
return 1
return fib(n-1)+fib(n-2)
<1>、多程序
列印第25到35個斐波那契數,並計算程式執行時間
import time from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor def fib(n): if n<=2: return 1 return fib(n-1)+fib(n-2) if __name__ == "__main__": with ProcessPoolExecutor(3) as executor: # 使用程序池控制 每次執行3個程序 all_task = [executor.submit(fib, (num)) for num in range(25,35)] start_time = time.time() for future in as_completed(all_task): data = future.result() print("exe result: {}".format(data)) print("last time is: {}".format(time.time()-start_time)) # 輸出 exe result: 75025 exe result: 121393 exe result: 196418 exe result: 317811 exe result: 514229 exe result: 832040 exe result: 1346269 exe result: 2178309 exe result: 3524578 exe result: 5702887 last time is: 4.457437038421631
輸出結果,每次列印三個exe result,總重列印十個結果,多程序執行時間為4.45秒
<2>、多執行緒
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ProcessPoolExecutor
def fib(n):
if n<=2:
return 1
return fib(n-1)+fib(n-2)
if __name__ == "__main__":
with ThreadPoolExecutor(3) as executor: # 使用執行緒池控制 每次執行3個執行緒
all_task = [executor.submit(fib, (num)) for num in range(25,35)]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 輸出
exe result: 121393
exe result: 75025
exe result: 196418
exe result: 317811
exe result: 514229
exe result: 832040
exe result: 1346269
exe result: 2178309
exe result: 3524578
exe result: 5702887
last time is: 7.3467772006988525
最終程式執行時間為7.34秒
程式的執行之間與計算機的效能有關,每天計算機的執行時間都會有差異。從上述結果中看顯然多執行緒比多程序要耗費時間。這就是因為對於密集程式碼(密集運算,迴圈語句等),tick計數很快達到100,GIL來回的釋放競爭,執行緒之間頻繁切換,所以對於密集程式碼的執行中,多執行緒效能不如對程序。
2、I/O密集程式碼
一個執行緒在I/O阻塞的時候,會釋放GIL,掛起,然後其他的執行緒會競爭CPU資源,涉及到執行緒的切換,但是這種代價與較高時延的I/O來說是不足為道的。
下面用sleep函式模擬密集I/O
def random_sleep(n):
time.sleep(n)
return n
<1>、 多程序
def random_sleep(n):
time.sleep(n)
return n
if __name__ == "__main__":
with ProcessPoolExecutor(5) as executor:
all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 輸出
exe result: 2
exe result: 2
......(30個)
exe result: 2
exe result: 2
last time is: 12.412866353988647
每次列印5個結果,總共二十個列印結果,多程序執行時間為12.41秒
<2>、多執行緒
def random_sleep(n):
time.sleep(n)
return n
if __name__ == "__main__":
with ThreadPoolExecutor(5) as executor:
all_task = [executor.submit(random_sleep, (num)) for num in [2]*30]
start_time = time.time()
for future in as_completed(all_task):
data = future.result()
print("exe result: {}".format(data))
print("last time is: {}".format(time.time()-start_time))
# 輸出
exe result: 2
exe result: 2
......(30個)
exe result: 2
exe result: 2
last time is: 12.004231214523315
I/O密集多執行緒情況下,程式的效能較多程序有了略微的提高。IO密集型程式碼(檔案處理、網路爬蟲等),多執行緒能夠有效提升效率(單執行緒下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多執行緒能線上程A等待時,自動切換到執行緒B,可以不浪費CPU的資源,從而能提升程式執行效率)。所以python的多執行緒對IO密集型程式碼比較友好。
3、總結
- CPU密集型程式碼(各種迴圈處理、計數等等),多執行緒效能不如多程序。
- I/O密集型程式碼(檔案處理、網路爬蟲等),多程序不如多執行緒。