網(wǎng)站首頁 編程語言 正文
前言
GIL(Global Interpreter Lock),全局解釋器鎖,是 CPython 為了避免在多線程環(huán)境下造成 Python 解釋器內(nèi)部數(shù)據(jù)的不一致而引入的一把鎖,讓 Python 中的多個線程交替運(yùn)行,避免競爭。
需要說明的是 GIL 不是 Python 語言規(guī)范的一部分,只是由于 CPython 實現(xiàn)的需要而引入的,其他的實現(xiàn)如 Jython 和 PyPy 是沒有 GIL 的。那么為什么 CPython 需要 GIL 呢,下面我們就來一探究竟(基于 CPython 3.10.4)。
為什么需要 GIL
GIL 本質(zhì)上是一把鎖,學(xué)過操作系統(tǒng)的同學(xué)都知道鎖的引入是為了避免并發(fā)訪問造成數(shù)據(jù)的不一致。CPython 中有很多定義在函數(shù)外面的全局變量,比如內(nèi)存管理中的?usable_arenas?和?usedpools,如果多個線程同時申請內(nèi)存就可能同時修改這些變量,造成數(shù)據(jù)錯亂。另外 Python 的垃圾回收機(jī)制是基于引用計數(shù)的,所有對象都有一個?ob_refcnt字段表示當(dāng)前有多少變量會引用當(dāng)前對象,變量賦值、參數(shù)傳遞等操作都會增加引用計數(shù),退出作用域或函數(shù)返回會減少引用計數(shù)。同樣地,如果有多個線程同時修改同一個對象的引用計數(shù),就有可能使?ob_refcnt?與真實值不同,可能會造成內(nèi)存泄漏,不會被使用的對象得不到回收,更嚴(yán)重可能會回收還在被引用的對象,造成 Python 解釋器崩潰。
GIL 的實現(xiàn)
CPython 中 GIL 的定義如下
struct _gil_runtime_state {
unsigned long interval; // 請求 GIL 的線程在 interval 毫秒后還沒成功,就會向持有 GIL 的線程發(fā)出釋放信號
_Py_atomic_address last_holder; // GIL 上一次的持有線程,強(qiáng)制切換線程時會用到
_Py_atomic_int locked; // GIL 是否被某個線程持有
unsigned long switch_number; // GIL 的持有線程切換了多少次
// 條件變量和互斥鎖,一般都是成對出現(xiàn)
PyCOND_T cond;
PyMUTEX_T mutex;
// 條件變量,用于強(qiáng)制切換線程
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
};
最本質(zhì)的是 mutex 保護(hù)的 locked 字段,表示 GIL 當(dāng)前是否被持有,其他字段是為了優(yōu)化 GIL 而被用到的。線程申請 GIL 時會調(diào)用?take_gil()?方法,釋放 GIL時 調(diào)用?drop_gil()?方法。為了避免饑餓現(xiàn)象,當(dāng)一個線程等待了 interval 毫秒(默認(rèn)是 5 毫秒)還沒申請到 GIL 的時候,就會主動向持有 GIL 的線程發(fā)出信號,GIL 的持有者會在恰當(dāng)時機(jī)檢查該信號,如果發(fā)現(xiàn)有其他線程在申請就會強(qiáng)制釋放 GIL。這里所說的恰當(dāng)時機(jī)在不同版本中有所不同,早期是每執(zhí)行 100 條指令會檢查一次,在 Python 3.10.4 中是在條件語句結(jié)束、循環(huán)語句的每次循環(huán)體結(jié)束以及函數(shù)調(diào)用結(jié)束的時候才會去檢查。
申請 GIL 的函數(shù)?take_gil()?簡化后如下
static void take_gil(PyThreadState *tstate)
{
...
// 申請互斥鎖
MUTEX_LOCK(gil->mutex);
// 如果 GIL 空閑就直接獲取
if (!_Py_atomic_load_relaxed(&gil->locked)) {
goto _ready;
}
// 嘗試等待
while (_Py_atomic_load_relaxed(&gil->locked)) {
unsigned long saved_switchnum = gil->switch_number;
unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
int timed_out = 0;
COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
if (timed_out && _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) {
SET_GIL_DROP_REQUEST(interp);
}
}
_ready:
MUTEX_LOCK(gil->switch_mutex);
_Py_atomic_store_relaxed(&gil->locked, 1);
_Py_ANNOTATE_RWLOCK_ACQUIRED(&gil->locked, /*is_write=*/1);
if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) {
_Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
++gil->switch_number;
}
// 喚醒強(qiáng)制切換的線程主動等待的條件變量
COND_SIGNAL(gil->switch_cond);
MUTEX_UNLOCK(gil->switch_mutex);
if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
RESET_GIL_DROP_REQUEST(interp);
}
else {
COMPUTE_EVAL_BREAKER(interp, ceval, ceval2);
}
...
// 釋放互斥鎖
MUTEX_UNLOCK(gil->mutex);
}
整個函數(shù)體為了保證原子性,需要在開頭和結(jié)尾分別申請和釋放互斥鎖?gil->mutex。如果當(dāng)前 GIL 是空閑狀態(tài)就直接獲取 GIL,如果不空閑就等待條件變量?gil->cond?interval 毫秒(不小于 1 毫秒),如果超時并且期間沒有發(fā)生過 GIL 切換就將?gil_drop_request?置位,請求強(qiáng)制切換 GIL 持有線程,否則繼續(xù)等待。一旦獲取 GIL 成功需要更新?gil->locked、gil->last_holder?和?gil->switch_number?的值,喚醒條件變量?gil->switch_cond,并且釋放互斥鎖?gil->mutex。
釋放 GIL 的函數(shù)?drop_gil()?簡化后如下
static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
PyThreadState *tstate)
{
...
if (tstate != NULL) {
_Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
}
MUTEX_LOCK(gil->mutex);
_Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
// 釋放 GIL
_Py_atomic_store_relaxed(&gil->locked, 0);
// 喚醒正在等待 GIL 的線程
COND_SIGNAL(gil->cond);
MUTEX_UNLOCK(gil->mutex);
if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request) && tstate != NULL) {
MUTEX_LOCK(gil->switch_mutex);
// 強(qiáng)制等待一次線程切換才被喚醒,避免饑餓
if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
{
assert(is_tstate_valid(tstate));
RESET_GIL_DROP_REQUEST(tstate->interp);
COND_WAIT(gil->switch_cond, gil->switch_mutex);
}
MUTEX_UNLOCK(gil->switch_mutex);
}
}
首先在?gil->mutex?的保護(hù)下釋放 GIL,然后喚醒其他正在等待 GIL 的線程。在多 CPU 的環(huán)境下,當(dāng)前線程在釋放 GIL 后有更高的概率重新獲得 GIL,為了避免對其他線程造成饑餓,當(dāng)前線程需要強(qiáng)制等待條件變量?gil->switch_cond,只有在其他線程獲取 GIL 的時候當(dāng)前線程才會被喚醒。
幾點說明
GIL 優(yōu)化
受 GIL 約束的代碼不能并行執(zhí)行,降低了整體性能,為了盡量降低性能損失,Python 在進(jìn)行 IO 操作或不涉及對象訪問的密集 CPU 計算的時候,會主動釋放 GIL,減小了 GIL 的粒度,比如
- 讀寫文件
- 網(wǎng)絡(luò)訪問
- 加密數(shù)據(jù)/壓縮數(shù)據(jù)
所以嚴(yán)格來說,在單進(jìn)程的情況下,多個 Python 線程時可能同時執(zhí)行的,比如一個線程在正常運(yùn)行,另一個線程在壓縮數(shù)據(jù)。
用戶數(shù)據(jù)的一致性不能依賴 GIL
GIL 是為了維護(hù) Python 解釋器內(nèi)部變量的一致性而產(chǎn)生的鎖,用戶數(shù)據(jù)的一致性不由 GIL 負(fù)責(zé)。雖然 GIL 在一定程度上也保證了用戶數(shù)據(jù)的一致性,比如 Python 3.10.4 中不涉及跳轉(zhuǎn)和函數(shù)調(diào)用的指令都會在 GIL 的約束下原子性的執(zhí)行,但是數(shù)據(jù)在業(yè)務(wù)邏輯上的一致性需要用戶自己加鎖來保證。
下面的代碼用兩個線程模擬用戶集碎片得獎
from threading import Thread
def main():
stat = {"piece_count": 0, "reward_count": 0}
t1 = Thread(target=process_piece, args=(stat,))
t2 = Thread(target=process_piece, args=(stat,))
t1.start()
t2.start()
t1.join()
t2.join()
print(stat)
def process_piece(stat):
for i in range(10000000):
if stat["piece_count"] % 10 == 0:
reward = True
else:
reward = False
if reward:
stat["reward_count"] += 1
stat["piece_count"] += 1
if __name__ == "__main__":
main()
假設(shè)用戶每集齊 10 個碎片就能得到一次獎勵,每個線程收集了 10000000 個碎片,應(yīng)該得到 9999999 個獎勵(最后一次沒有計算),總共應(yīng)該收集 20000000 個碎片,得到 1999998 個獎勵,但是在我電腦上一次運(yùn)行結(jié)果如下
{'piece_count': 20000000, 'reward_count': 1999987}
總的碎片數(shù)量與預(yù)期一致,但是獎勵數(shù)量卻少了 12 個。碎片數(shù)量正確是因為在 Python 3.10.4 中,stat["piece_count"] += 1?是在 GIL 約束下原子性執(zhí)行的。由于每次循環(huán)結(jié)束都可能切換執(zhí)行線程,那么可能線程 t1 在某次循環(huán)結(jié)束時將?piece_count?加到 100,但是在下次循環(huán)開始模 10 判斷前,Python 解釋器切換到線程 t2 執(zhí)行,t2 將?piece_count?加到 101,那么就會錯過一次獎勵。
附:如何避免受到GIL的影響
說了那么多,如果不說解決方案就僅僅是個科普帖,然并卵。GIL這么爛,有沒有辦法繞過呢?我們來看看有哪些現(xiàn)成的方案。
用multiprocess替代Thread
multiprocess庫的出現(xiàn)很大程度上是為了彌補(bǔ)thread庫因為GIL而低效的缺陷。它完整的復(fù)制了一套thread所提供的接口方便遷移。唯一的不同就是它使用了多進(jìn)程而不是多線程。每個進(jìn)程有自己的獨立的GIL,因此也不會出現(xiàn)進(jìn)程之間的GIL爭搶。
當(dāng)然multiprocess也不是萬能良藥。它的引入會增加程序?qū)崿F(xiàn)時線程間數(shù)據(jù)通訊和同步的困難。就拿計數(shù)器來舉例子,如果我們要多個線程累加同一個變量,對于thread來說,申明一個global變量,用thread.Lock的context包裹住三行就搞定了。而multiprocess由于進(jìn)程之間無法看到對方的數(shù)據(jù),只能通過在主線程申明一個Queue,put再get或者用share memory的方法。這個額外的實現(xiàn)成本使得本來就非常痛苦的多線程程序編碼,變得更加痛苦了。具體難點在哪有興趣的讀者可以擴(kuò)展閱讀這篇文章
用其他解析器
之前也提到了既然GIL只是CPython的產(chǎn)物,那么其他解析器是不是更好呢?沒錯,像JPython和IronPython這樣的解析器由于實現(xiàn)語言的特性,他們不需要GIL的幫助。然而由于用了Java/C#用于解析器實現(xiàn),他們也失去了利用社區(qū)眾多C語言模塊有用特性的機(jī)會。所以這些解析器也因此一直都比較小眾。畢竟功能和性能大家在初期都會選擇前者,Done is better than perfect。
所以沒救了么?
當(dāng)然Python社區(qū)也在非常努力的不斷改進(jìn)GIL,甚至是嘗試去除GIL。并在各個小版本中有了不少的進(jìn)步。有興趣的讀者可以擴(kuò)展閱讀這個Slide
另一個改進(jìn)Reworking the GIL
– 將切換顆粒度從基于opcode計數(shù)改成基于時間片計數(shù)
– 避免最近一次釋放GIL鎖的線程再次被立即調(diào)度
– 新增線程優(yōu)先級功能(高優(yōu)先級線程可以迫使其他線程釋放所持有的GIL鎖)
總結(jié)
GIL 是 CPython 為了在多線程環(huán)境下為了維護(hù)解釋器內(nèi)部數(shù)據(jù)一致性而引入的,為了盡可能降低 GIL 的粒度,在 IO 操作和不涉及對象訪問的 CPU 計算時會主動釋放 GIL。最后,用戶數(shù)據(jù)的一致性不能依賴 GIL,可能需要用戶使用?Lock?或?RLock()?來保證數(shù)據(jù)的原子性訪問。
到此這篇關(guān)于對Python中GIL理解的文章就介紹到這了,更多相關(guān)Python中GIL理解內(nèi)容請搜索AB教程網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持AB教程網(wǎng)!
參考文檔
- https://realpython.com/python-gil/
- https://github.com/python/cpython/commit/074e5ed974be65fbcfe75a4c0529dbc53f13446f
- https://mail.python.org/pipermail/python-dev/2009-October/093321.html
- https://www.backblaze.com/blog/the-python-gil-past-present-and-future/
原文鏈接:https://www.cnblogs.com/zikcheng/p/16319676.html
相關(guān)推薦
- 2022-04-08 Unity?UGUI?按鈕綁定事件的?4?種方式匯總_C#教程
- 2023-01-15 nx.adjacency_matrix計算鄰接矩陣與真實結(jié)果不一致的解決_python
- 2022-04-21 IDEA - Spring Boot 項目 application.yml 文件不加載的問題
- 2023-07-15 react+antd+table實現(xiàn)表格數(shù)據(jù)在當(dāng)前頁從頭到尾循環(huán)滾動展示
- 2022-09-29 Shell之function函數(shù)的定義及調(diào)用示例_linux shell
- 2022-03-14 Failed to load ApplicationContext
- 2022-03-19 Docker?link實現(xiàn)容器互聯(lián)的方式_docker
- 2022-06-26 ASP.NET?Core中間件會話狀態(tài)讀寫及生命周期示例_實用技巧
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支