Python GIL全域性直譯器鎖詳解(深度剖析)

2020-07-16 10:05:03
通過前面的學習,我們了解了 Pyton 並行程式設計的特性以及什麼是多執行緒程式設計。其實除此之外,Python 多執行緒還有一個很重要的知識點,就是本節要講的 GIL。

GIL,中文譯為全域性直譯器鎖。在講解 GIL 之前,首先通過一個例子來直觀感受一下 GIL 在 Python 多執行緒程式執行的影響。

首先執行如下程式:
import time
start = time.clock()
def CountDown(n):
    while n > 0:
        n -= 1
CountDown(100000)
print("Time used:",(time.clock() - start))
執行結果為:

Time used: 0.0039529000000000005


在我們的印象中,使用多個(適量)執行緒是可以加快程式執行效率的,因此可以嘗試將上面程式改成如下方式:
import time
from threading import Thread
start = time.clock()
def CountDown(n):
    while n > 0:
        n -= 1
t1 = Thread(target=CountDown, args=[100000 // 2])
t2 = Thread(target=CountDown, args=[100000 // 2])
t1.start()
t2.start()
t1.join()
t2.join()
print("Time used:",(time.clock() - start))
執行結果為:

Time used: 0.006673

可以看到,此程式中使用了 2 個執行緒來執行和上面程式碼相同的工作,但從輸出結果中可以看到,執行效率非但沒有提高,反而降低了。

如果使用更多執行緒進行嘗試,會發現其執行效率和 2 個執行緒效率幾乎一樣(本機器測試使用 4 個執行緒,其執行效率約為 0.005)。這裡不再給出具體測試程式碼,有興趣的讀者可自行測試。

是不是和你猜想的結果不一樣?事實上,得到這樣的結果是肯定的,因為 GIL 限制了 Python 多執行緒的效能不會像我們預期的那樣。

那麼,什麼是 GIL 呢?GIL 是最流程的 CPython 直譯器(平常稱為 Python)中的一個技術術語,中文譯為全域性直譯器鎖,其本質上類似作業系統的 Mutex。GIL 的功能是:在 CPython 直譯器中執行的每一個 Python 執行緒,都會先鎖住自己,以阻止別的執行緒執行。

當然,CPython 不可能容忍一個執行緒一直獨占直譯器,它會輪流執行 Python 執行緒。這樣一來,使用者看到的就是“偽”並行,即 Python 執行緒在交替執行,來模擬真正並行的執行緒。

有讀者可能會問,既然 CPython 能控制執行緒偽並行,為什麼還需要 GIL 呢?其實,這和 CPython 的底層記憶體管理有關。

CPython 使用參照計數來管理內容,所有 Python 指令碼中建立的範例,都會配備一個參照計數,來記錄有多少個指標來指向它。當範例的參照計數的值為 0 時,會自動釋放其所佔的記憶體。

舉個例子,看如下程式碼:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

可以看到,a 的參照計數值為 3,因為有 a、b 和作為引數傳遞的 getrefcount 都參照了一個空列表。

假設有兩個 Python 執行緒同時參照 a,那麼雙方就都會嘗試操作該資料,很有可能造成參照計數的條件競爭,導致參照計數只增加 1(實際應增加 2),這造成的後果是,當第一個執行緒結束時,會把參照計數減少 1,此時可能已經達到釋放記憶體的條件(參照計數為 0),當第 2 個執行緒再次檢視存取 a 時,就無法找到有效的記憶體了。

所以,CPython 引進 GIL,可以最大程度上規避類似記憶體管理這樣複雜的競爭風險問題。

Python GIL底層實現原理

GIL工作流程示意圖
圖 1 GIL 工作流程示意圖