Python3 原始碼閱讀-深入瞭解Python GIL

JonPan發表於2020-06-09

今日得到: 三人行,必有我師焉,擇其善者而從之,其不善者而改之。

現在已經是2020年了,而在2010年的時候,大佬David Beazley就做了講座講解Python GIL的設計相關問題,10年間相信也在不斷改善和優化,但是並沒有將GIL從CPython中移除,可想而知,GIL已經深入CPython,難以移除。就目前來看,工作中常用的還是協程,多執行緒來處理高併發的I/O密集型任務。CPU密集型的大型計算可以用其他語言來實現。

1. GIL

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) ----- Global Interpreter Lock

為了防止多執行緒共享記憶體出現競態問題,設定的防止多執行緒併發執行機器碼的一個Mutex。

2. python32 之前-基於opcode數量的排程方式

在python3.2版本之前,定義了一個tick計數器,表示當前執行緒在釋放gil之前連續執行的多少個位元組碼(實際上有部分執行較快的位元組碼並不會被計入計數器)。如果當前的執行緒正在執行一個 CPU 密集型的任務, 它會在 tick 計數器到達 100 之後就釋放 gil, 給其他執行緒一個獲得 gil 的機會。

old_gil

(圖片來自 Understanding the Python GIL(youtube))

以opcode個數為基準來計數,如果有些opcode程式碼複雜耗時較長,一些耗時較短,會導致同樣的100個tick,一些執行緒的執行時間總是執行的比另一些長。是不公平的排程策略。

image.png

(圖片來自Understanding-the-python-gil

如果當前的執行緒正在執行一個 IO密集型的 的任務, 你執行 sleep/recv/send(...etc) 這些會阻塞的系統呼叫時, 即使 tick 計數器的值還沒到 100, gil 也會被主動地釋放。至於下次該執行哪一個執行緒這個是作業系統層面的,執行緒排程演算法優先順序排程,開發者沒辦法控制。

在多核機器上, 如果兩個執行緒都在執行 CPU 密集型的任務, 作業系統有可能讓這兩個執行緒在不同的核心上執行, 也許會出現以下的情況, 當一個擁有了 gil 的執行緒在一個核心上執行 100 次 tick 的過程中, 在另一個核心上執行的執行緒頻繁的進行搶佔 gil, 搶佔失敗的迴圈, 導致 CPU 瞎忙影響效能。 如下圖:綠色部分表示該執行緒在執行,且在執行有用的計算,紅色部分為執行緒被排程喚醒,但是無法獲取GIL導致無法進行有效運算等待的時間。

image.png

由圖可見,GIL的存在導致多執行緒無法很好的利用多核CPU的併發處理能力。

3. python3.2 之後-基於時間片的切換

由於在多核機器下可能導致效能下降, gil的實現在python3.2之後做了一些優化 。python在初始化直譯器的時候就會初始化一個gil,並設定一個DEFAULT_INTERVAL=5000, 單位是微妙,即0.005秒(在 C 裡面是用 微秒 為單位儲存, 在 python 直譯器中以秒來表示)這個間隔就是GIL切換的標誌。

// Python\ceval_gil.h
#define DEFAULT_INTERVAL 5000

static void _gil_initialize(struct _gil_runtime_state *gil)
{
    _Py_atomic_int uninitialized = {-1};
    gil->locked = uninitialized;
    gil->interval = DEFAULT_INTERVAL;
}

python中檢視gil切換的時間

In [7]: import sys
In [8]: sys.getswitchinterval()
Out[8]: 0.005

如果當前有不止一個執行緒, 當前等待 gil 的執行緒在超過一定時間的等待後, 會把全域性變數 gil_drop_request 的值設定為 1, 之後繼續等待相同的時間, 這時擁有 gil 的執行緒看到了 gil_drop_request 變為 1, 就會主動釋放 gil 並通過 condition variable 通知到在等待中的執行緒, 第一個被喚醒的等待中的執行緒會搶到 gil 並執行相應的任務, 將gil_drop_request設定為1的執行緒不一定能搶到gil

image.png

4 condition variable相關欄位

  1. locked : locked 的型別是_Py_atomic_int, 值-1表示還未初始化,0表示當前的gil處於釋放狀態,1表示某個執行緒已經佔用了gil,這個值的型別設定為原子型別之後在 ceval.c 就可以不加鎖的對這個值進行讀取。
  2. interval:是執行緒在設定gil_drop_request這個變數之前需要等待的時長,預設是5000毫秒
  3. last_holder:存放了最後一個持有 gil 的執行緒的 C 中對應的 PyThreadState 結構的指標地址, 通過這個值我們可以知道當前執行緒釋放了 gil 後, 是否有其他執行緒獲得了 gil(可以採取措施避免被自己重新獲得)
  4. switch_number: 是一個計數器, 表示從直譯器執行到現在, gil 總共被釋放獲得多少次
  5. mutex:是一把互斥鎖, 用來保護 locked, last_holder, switch_number 還有 _gil_runtime_state 中的其他變數
  6. cond:是一個 condition variable, 和 mutex 結合起來一起使用, 當前執行緒釋放 gil 時用來給其他等待中的執行緒傳送訊號
  7. ** switch_cond and switch_mutex**

switch_cond 是另一個 condition variable, 和 switch_mutex 結合起來可以用來保證釋放後重新獲得 gil 的執行緒不是同一個前面釋放 gil 的執行緒, 避免 gil 切換時執行緒未切換浪費 cpu 時間

這個功能如果編譯時未定義 FORCE_SWITCHING 則不開啟

static void
drop_gil(struct _ceval_runtime_state *ceval, PyThreadState *tstate)
{
    ...

#ifdef FORCE_SWITCHING
    if (_Py_atomic_load_relaxed(&ceval->gil_drop_request) && tstate != NULL) {
        MUTEX_LOCK(gil->switch_mutex);
        /* Not switched yet => wait */
        if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
        {   
            /* 如果 last_holder 是當前執行緒, 釋放 switch_mutex 這把互斥鎖, 等待 switch_cond 這個條件變數的訊號 */
            RESET_GIL_DROP_REQUEST(ceval);
            /* NOTE: if COND_WAIT does not atomically start waiting when
               releasing the mutex, another thread can run through, take
               the GIL and drop it again, and reset the condition
               before we even had a chance to wait for it. */
            /* 注意, 如果 COND_WAIT 不在互斥鎖釋放後原子的啟動,
                另一個執行緒有可能會在這中間拿到 gil 並釋放,
            '並且重置這個條件變數, 這個過程發生在了 COND_WAIT 之前 */
            COND_WAIT(gil->switch_cond, gil->switch_mutex);
        }
        MUTEX_UNLOCK(gil->switch_mutex);
    }
#endif
}

4. gil在main_loop中的體現

//
main_loop:
for (;;) {
    /* 如果 gil_drop_request 被其他執行緒設定為 1 */
    /* 給其他執行緒一個獲得 gil 的機會 */
    if (_Py_atomic_load_relaxed(&ceval->gil_drop_request)) {
    /* Give another thread a chance */
    if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
        Py_FatalError("ceval: tstate mix-up");
    }
    drop_gil(ceval, tstate);

    /* Other threads may run now */

    take_gil(ceval, tstate);

    /* Check if we should make a quick exit. */
    exit_thread_if_finalizing(runtime, tstate);

    if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) {
        Py_FatalError("ceval: orphan tstate");
        }
    }
    /* Check for asynchronous exceptions. */
    /* 忽略 */
    fast_next_opcode:
    switch (opcode) {
        case TARGET(NOP): {
            FAST_DISPATCH();
        }
        /* 忽略 */
        case TARGET(UNARY_POSITIVE): {
            PyObject *value = TOP();
            PyObject *res = PyNumber_Positive(value);
            Py_DECREF(value);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }
    	/* 忽略 */
    }
    /* 忽略 */
}

這個很大的 for loop 會按順序逐個的載入 opcode, 並委派給中間很大的 switch statement 去進行執行, switch statement 會根據不同的 opcode 跳轉到不同的位置執行

for loop在開始位置會檢查 gil_drop_request變數, 必要的時候會釋放 gil

不是所有的 opcode 執行之前都會檢查 gil_drop_request 的, 有一些 opcode 結束時的程式碼為 FAST_DISPATCH(), 這部分 opcode 會直接跳轉到下一個 opcode 對應的程式碼的部分進行執行

而另一些 DISPATCH() 結尾的作用和 continue 類似, 會跳轉到 for loop 頂端, 重新檢測 gil_drop_request, 必要時釋放 gil

5 如何解決GIL

GIL只會對CPU密集型的程式產生影響,規避GIL限制主要有兩種常用策略:一是使用多程式,二是使用C語言擴充套件,把計算密集型的任務轉移到C語言中,使其獨立於Python,在C程式碼中釋放GIL。當然也可以使用其他語言編譯的直譯器如 JpythonPyPy

6.總結

  1. Python語言和GIL沒有半毛錢關係,僅僅是由於歷史原因在CPython直譯器中難以移除GIL
  2. GIL:全域性直譯器鎖,每個執行緒在執行的過程都需要先獲取GIL,確保同一時刻僅有一個執行緒執行程式碼,所以python的執行緒無法利用多核。
  3. 執行緒在I/O操作等可能引起阻塞的system call之前,可以暫時釋放GIL,執行完畢後重新獲取GIL,python3.2以後使用時間片來切換執行緒,時間閾值是0.005秒,而python3.2之前是使用opcode執行的數量(tick=100)來切換的。
  4. Python的多執行緒在多核CPU上,只對於IO密集型計算產生正面效果;而當有至少有一個CPU密集型執行緒存在,那麼多執行緒效率會由於GIL而大幅下降

參考

Cpython-gil講解-zpoint

Python的GIL是什麼鬼-盧鈞軼(cenalulu)

Youtube-Understanding the Python GIL

相關文章