今日獲得: 三人行,必有我師焉,擇其善者而從之,其不善者而改之。python
如今已是2020年了,而在2010年的時候,大佬David Beazley就作了講座講解Python GIL的設計相關問題,10年間相信也在不斷改善和優化,可是並無將GIL從CPython中移除,可想而知,GIL已經深刻CPython,難以移除。就目前來看,工做中經常使用的仍是協程,多線程來處理高併發的I/O密集型任務。CPU密集型的大型計算能夠用其餘語言來實現。git
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 Lockgithub
爲了防止多線程共享內存出現競態問題,設置的防止多線程併發執行機器碼的一個Mutex。算法
在python3.2版本以前,定義了一個tick計數器,表示當前線程在釋放gil以前連續執行的多少個字節碼(實際上有部分執行較快的字節碼並不會被計入計數器)。若是當前的線程正在執行一個 CPU 密集型的任務, 它會在 tick 計數器到達 100 以後就釋放 gil, 給其餘線程一個得到 gil 的機會。shell
(圖片來自 Understanding the Python GIL(youtube))多線程
以opcode個數爲基準來計數,若是有些opcode代碼複雜耗時較長,一些耗時較短,會致使一樣的100個tick,一些線程的執行時間老是執行的比另外一些長。是不公平的調度策略。併發
(圖片來自Understanding-the-python-gil)async
若是當前的線程正在執行一個 IO密集型的 的任務, 你執行 sleep/recv/send(...etc)
這些會阻塞的系統調用時, 即便 tick 計數器的值還沒到 100, gil 也會被主動地釋放。至於下次該執行哪個線程這個是操做系統層面的,線程調度算法優先級調度,開發者沒辦法控制。ide
在多核機器上, 若是兩個線程都在執行 CPU 密集型的任務, 操做系統有可能讓這兩個線程在不一樣的核心上運行, 也許會出現如下的狀況, 當一個擁有了 gil 的線程在一個核心上執行 100 次 tick 的過程當中, 在另外一個核心上運行的線程頻繁的進行搶佔 gil, 搶佔失敗的循環, 致使 CPU 瞎忙影響性能。 以下圖:綠色部分表示該線程在運行,且在執行有用的計算,紅色部分爲線程被調度喚醒,可是沒法獲取GIL致使沒法進行有效運算等待的時間。高併發
由圖可見,GIL的存在致使多線程沒法很好的利用多核CPU的併發處理能力。
因爲在多核機器下可能致使性能降低, 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
_Py_atomic_int
, 值-1表示還未初始化,0表示當前的gil處於釋放狀態,1表示某個線程已經佔用了gil,這個值的類型設置爲原子類型以後在 ceval.c
就能夠不加鎖的對這個值進行讀取。gil_drop_request
這個變量以前須要等待的時長,默認是5000毫秒locked
, last_holder
, switch_number
還有 _gil_runtime_state
中的其餘變量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 }
// 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
。
GIL只會對CPU密集型的程序產生影響,規避GIL限制主要有兩種經常使用策略:一是使用多進程,二是使用C語言擴展,把計算密集型的任務轉移到C語言中,使其獨立於Python,在C代碼中釋放GIL。固然也可使用其餘語言編譯的解釋器如 Jpython
、PyPy
。
參考