GIL與多執行緒

Jarvis_You發表於2018-11-12

什麼是GIL?

GIL全稱global interpreter lock,全域性直譯器鎖。每一個執行緒在執行之前都需要獲取GIL許可權才可執行程式碼,也就是說在多執行緒中,實際上同一時間只有一個執行緒在執行。GIL並非是python自帶的特性,而是Cpython直譯器引入的一個概念,在Jpython(Java實現的python直譯器)中就沒有GIL。

這是一個歷史遺留問題,如今大量開發者習慣了這套機制,程式碼量也越來越多 ,已經不容易通過修改Cpython來解決這個問題。

並行與併發

在進一步瞭解GIL之前,我們先回顧一下並行與併發的概念。

並行:同一時間,可以處理多個任務,多個任務是一起被執行。

併發:同一時間,不能處理多個任務,但是可以交替處理多個任務。

舉個簡單的例子:

你邊寫作業,邊玩手機,說明你支援並行。

你寫5分鐘作業然後玩10分鐘手機,再寫5分鐘,再玩10分鐘,說明你支援併發。

他們雖然方式不同,但都指向了一個點,多工執行,目的是要提高CPU的使用效率。這裡需要注意 一點,單核CPU永遠無法實現並行,一個CPU無法同時執行多個程式,但是可以實現併發。在多執行緒執行程式碼過程中會帶來一個問題,執行緒間的資料一致性和狀態同步完整性。(補充說明:子程式在開始後,他們的執行是隨機的,並沒有先後順序)解決這個問題最簡單的方案,就是加上GIL這把全域性大鎖。因為有了GIL的存在,python中多執行緒的效率會大打折扣。甚至幾乎等於python就是單執行緒程式。

下面我們做一個python下多執行緒和單執行緒效率對比:

from threading import Thread
import time

def task():         #執行任務
    i = 0
    for _ in range(10000000):
        i = i + 1
    return True
複製程式碼

下面main1模仿的是序列執行任務,其中採用for迴圈建立執行緒主要是增加建立執行緒的時間,更準確的對比出多執行緒有GIL的差別。

def main1():        #單執行緒序列
    start_time = time.time()
    for tid in range(20):
        t = Thread(target=task)
        t.start()
        t.join()
    end_time = time.time()

    print("單執行緒耗時: {}".format(end_time - start_time))


def main2():        #多執行緒併發
    thread_array = {}
    start_time = time.time()
    for tid in range(20):
        t = Thread(target=task)
        t.start()
        thread_array[tid] = t
    for i in range(20):
        thread_array[i].join()
    end_time = time.time()

    print("多執行緒耗時: {}".format(end_time - start_time))


if __name__ == '__main__':
    main1()
    main2()

複製程式碼

測試CPU為i7 7700k,python版本為3.7,測試結果如下:

當任務為計算密集型時,多執行緒併發對比序列區別並不大。

單執行緒耗時: 16.951716423034668
多執行緒耗時: 16.735241651535034
複製程式碼

下面我們修改一下執行任務,暫停1秒模仿程式IO操作:

def task():         #執行任務
    time.sleep(1)
    return True
複製程式碼

執行結果如下:

對比明顯,多執行緒在執行IO密集型效率會比序列高很多。

單執行緒耗時: 20.019601345062256
多執行緒耗時: 1.0051548480987549
複製程式碼

接下來我們對比一下多執行緒與多程式在計算密集型和IO密集型的差異:

計算密集型

# 計算密集任務
def task():
    sum = 1
    for i in range(100000000):
        sum *= i
    pass
複製程式碼

IO密集型

# IO密集任務
def task():
    time.sleep(5)
    pass
複製程式碼
if __name__ == '__main__':      

    start_time = time.time()
    
    # 多執行緒
    t1 = Thread(target=task)
    t2 = Thread(target=task)
    t3 = Thread(target=task)
    t4 = Thread(target=task)
    t5 = Thread(target=task)
    t6 = Thread(target=task)
    # 多程式
    # t1 = Process(target=task)
    # t2 = Process(target=task)
    # t3 = Process(target=task)
    # t4 = Process(target=task)
    # t5 = Process(target=task)
    # t6 = Process(target=task)

    t1.start()
    t2.start()
    t3.start()
    t4.start()
    t5.start()
    t6.start()

    t1.join()
    t2.join()
    t3.join()
    t4.join()
    t5.join()
    t6.join()

    end_time = time.time()

    print("多執行緒耗時: {}".format(end_time - start_time))
複製程式碼

對比結果如下:

計算密集型

多執行緒耗時: 38.91398763656616
多程式耗時: 9.934444665908813
複製程式碼

IO密集型

多執行緒程耗時: 5.002830743789673
多程式程耗時: 5.4561707973480225
複製程式碼

在計算密集型中,多程式效率幾乎碾壓多執行緒,而在IO密集型多執行緒的優勢就體現出來了,並且隨著執行緒數量越多優勢越明顯。

總結: 經過實驗對比,我們不難發現在GIL下的多執行緒也有自己的特色,在IO場景下有較好的效能,而在其他方面和單執行緒差異不大,當遇到被迫需要多執行緒的場景下,也可以考慮用多程式來代替。Python GIL是功能和效能之間權衡後的產物,它尤其存在的合理性,也有較難改變的客觀因素。


ps:本文是學習之後的思考與總結,如有不足之處望指點。

相關文章