Python GIL(Global Interpreter Lock)
本文將介紹GIL是什麼,為什麼存在,對Python的影響是什麼,以及一些歷史演進。
本文主要參考David Beazley的2篇分享Inside the Python GIL與Inside the New GIL
Introduce
In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple 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.)
從上述中得知:
- GIL是一個控制Thread存取Python object的mutex
- GIL能確保同一時間只有一個Thread在Python interpreter內執行
- GIL存在的原因之一是因為CPython的memory management不是thread-safe
上述多次提到Thread,在Python中的Thread是一個Operating system的Thread,例如POSIX thread, Windows thread。
換言之,Python thread的scheduling與switching皆是由OS處理
Types of Threads
GIL與Thread密不可分,而GIL對不同種類的Thread的關係和影響也不同。
以下分別對兩種Thread分別闡述:
I/O bound threads
由圖得知(圖片來源:Inside the Python GIL):
- 在I/O bound threads執行前,並須先Acquire GIL
- 遇到I/O block時,會Release GIL
- 當GIL不被任何Thread持有時,則其他Thread有機會Acquire GIL
- 換言之,在I/O bound threads時,Multiple thread會比Single thread速度快
CPU bound threads
由圖得知(圖片來源:Inside the Python GIL):
- CPU bound不像I/O bound threads會被I/O block
- Python interpreter會定期執行Check
- 預設是100 interpreter ticks執行一次Check
- Tick:
- 由很多個Python interpreter instructions組成
- 不是基於時間來計算的,所以每個Tick的執行時間不相同
- Tick:
- 使用sys.setcheckinterval()更改此值
- 執行Check時讓當前持有GIL的Thread release GIL,則其他Thread有機會拿到GIL
- 預設是100 interpreter ticks執行一次Check
- 如果Main thread拿到了GIL,則會額外處理Signal,例如Interrupt signal
- 結合Python thread沒有Thread的scheduling能力,引發另一個問題
- 只有在Main thread拿到GIL時,該Program的Signal才能被處理
- 如果Main thread被不可中斷的Thread-join或Lock block時,Main thread永遠不會被OS安排到可被執行的狀態
- 所以Signal也永遠不會被處理
- 結合Python thread沒有Thread的scheduling能力,引發另一個問題
GIL Battle
如圖所示(圖片來源:Inside the Python GIL)
- T1/T2皆為CPU bound threads,且執行在不同的CPU core上
- 當T1在第一次Acquire GIL後,在每次的Check過程中,T1 release GIL之後,馬上又Acquire GIL並繼續執行,導致T2一直沒有機會Acquire GIL
- GIL Battle是因為下述兩個原則衝突導致
- OS的Thread scheduling的目的是要發揮Multiple cores的優勢
- GIL讓Python interpreter同時只能執行一個Thread
- 對I/O bound threads的影響
- 當I/O bound thread接收到I/O block結束的信號時,但另一個Core的CPU bound thread不斷的release/Acquire GIL
- I/O bound thread也就無法讀取I/O的結果
- 引發Priority inversion
- 在OS中,I/O bound thread的執行優先權高於CPU bound thread
- 但在發生上述情況時,I/O bound thread反而不會優先於CPU bound thread被執行完,此情況稱為Priority inversion
New GIL in Python3.2
Python 3.2的GIL進行了改善:
- Interpreter執行區間從Tick-based變更為Time-based
- 移除Tick機制,以及sys.setcheckinterval()
- 顯著的降低了Thrashing和Signaling的消耗
New Thread Switching
- 使用全局變量gil_drop_request決定是否進行Release GIL
- Running thread release GIL
- 被強制的Release GIL: gil_drop_request由0設定為1
- 自主的Release GIL: 進行I/O block或Sleep
- Suspended thread的狀態轉換
- 開始執行後,發現已經有其他Thread正在執行,無法Acquire GIL,則進入Suspended狀態,並設定該狀態維持T時長
- Case 1: 在T時間內接收到Release GIL signal後Acquire GIL
- Case 2: T時間內仍未接收到Release GIL signal,提出將gil_drop_request由0設定為1,則進入Suspended狀態,並設定該狀態維持T時長
- Case 2.1: 在T時間內接收到Release GIL signal後Acquire GIL
- Case 2.2: OS將Release GIL signal發送給其他Suspended thread,則自己回到Case 2等完T時長
- Thread在Acquire GIL的一段時間內,不會馬上被強制Release GIL
- 開始執行後,發現已經有其他Thread正在執行,無法Acquire GIL,則進入Suspended狀態,並設定該狀態維持T時長
- 若有多個Thread提出將gil_drop_request由0設定為1時,只有第一個要求會完成設置
- 提出要求的Thread,不一定可以Acquire GIL,可能被其他也在等待的Thread Acquire GIL
- T時長:
- 預設為5 milliseconds (0.005s)
- 使用sys.setswitchinterval()設定該值
- Time-based機制消除了GIL Battle
- Case 2參考(圖片來源:Inside the New GIL)
Interesting Features
- 新的GIL允許Acquire GIL的Thread無視其他Thread或I/O priority,持有GIL一個T時長
- 所以一個CPU-bound thread在整個操作週期中,會多次主動釋放GIL(例如多次Receive/Send data),而在每次Acquire GIL前最少都得等待一個T時長,導致I/O bound thread的總反應時間增加