1. 程式人生 > >python 程序、執行緒 (二)

python 程序、執行緒 (二)

一、多執行緒與多程序的對比

python 程序、執行緒 (一)中簡單的說過,CPython中的GIL使得同一時刻只能有一個執行緒執行,即併發執行。並且即使是多核CPU,GIL使得同一個程序中的多個執行緒也無法對映到多個CPU上執行,這麼做最初是為了安全著想,慢慢的也成為了限制CPython效能的問題。
就像是一個執行緒想要執行,就必須得到GIL,否則就不能拿到CPU資源。但是也不是說一個執行緒在拿到CPU資源後就一勞永逸,在執行的過程中GIL可能會釋放並被其他執行緒獲取,所以說其它的執行緒會與本執行緒競爭CPU資源。
understand GIL:http://www.dabeaz.com/python/UnderstandingGIL.pdf

中有關於GIL釋放和GIL的概要。
多執行緒在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密集型程式碼(檔案處理、網路爬蟲等),多程序不如多執行緒。