1. 程式人生 > >python 進程、線程 (二)

python 進程、線程 (二)

調整 文件處理 gil 什麽 代碼 說過 效率 釋放 complete

一、多線程與多進程的對比

在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密集型代碼(文件處理、網絡爬蟲等),多進程不如多線程。

python 進程、線程 (二)