CPython 有 GIL 是因?yàn)楫?dāng)年設(shè)計(jì) CPython 的人偷懶嗎? ① —— 簡(jiǎn)單的答案是:不僅沒有偷懶,相反 GIL 是一個(gè)杰出的設(shè)計(jì)。
一冕杠、Greg Stein 的嘗試
Guido van Rossum 提到 ② ,在 1999 年酸茴,Greg Stein(及 Mark Hammond ?)曾嘗試開發(fā)過一個(gè)無 GIL 的 Python(據(jù)信是 1.5 版)分支分预,該分支對(duì)“所有變量”施以細(xì)粒度線程鎖。這讓 Python 的單線程性能下降了兩倍薪捍,這足以抵消多線程所能為 Python 帶來的性能提升笼痹。
這次嘗試讓 Python 在取消 GIL 這件事上變得非常謹(jǐn)慎配喳,GIL 得以保留至今。但是反過來說凳干,我們發(fā)現(xiàn) Guido 在最初所選擇的 GIL —— 其實(shí)已經(jīng)是最優(yōu)方案了晴裹。
二、GIL 為什么快
拿數(shù)據(jù)庫來進(jìn)行類比纺座,使用粗粒度的 GIL 就好象在操作數(shù)據(jù)時(shí)息拜,直接鎖定整個(gè)數(shù)據(jù)庫溉潭。SQLite(非 WAL 模式)就是這樣一種直接鎖庫的實(shí)現(xiàn)净响,同時(shí) SQLite 也是在單線程下最快的主流 SQL 數(shù)據(jù)庫喳瓣。
使用細(xì)粒度鎖來代替 GIL馋贤,類似于鎖定到每個(gè)字段。由于過于影響性能畏陕,數(shù)據(jù)庫鎖定粒度一般不會(huì)做到這么細(xì)的程度配乓。而對(duì)于 Python 解釋器來說,由于很難從用戶代碼中抽離出類似于數(shù)據(jù)庫“行惠毁、表”級(jí)別的代碼段犹芹,來進(jìn)行中等粒度的鎖定,所以也就無法做到在不大量損失性能的情況下鞠绰,來去除 GIL 了腰埂。
我們?cè)趯?shí)際開發(fā)中,經(jīng)常會(huì)遇到類似的粒度選擇問題蜈膨。有經(jīng)驗(yàn)的開發(fā)者習(xí)慣上會(huì)采用一些“簡(jiǎn)單粗暴”的方法來解決問題屿笼,雖然看上去這很初級(jí),但是這樣做卻是最安全翁巍、穩(wěn)定驴一,和高效的。相反許多初學(xué)者反而會(huì)采用很多復(fù)雜灶壶、“高端”的方法來解決問題肝断,這些代碼往往都不夠安全、穩(wěn)定驰凛,同時(shí)又很低效孝情。
事實(shí)往往和表面上看到的不太一樣。對(duì)于有經(jīng)驗(yàn)的開發(fā)者來說洒嗤,簡(jiǎn)單粗暴的大規(guī)模數(shù)據(jù)鎖箫荡、GIL 等方案,其實(shí)是經(jīng)常會(huì)被用到的渔隶。這往往是一種下意識(shí)的選擇羔挡,背后是有豐富的開發(fā)經(jīng)驗(yàn)來支撐的洁奈,并不是偷懶的結(jié)果。
三绞灼、PyPy 的嘗試
PyPy 使用 STM(Software Transactional Memory)來去除 GIL利术,這有點(diǎn)像 Python 的 ZODB 數(shù)據(jù)庫基于 Conflict 的并發(fā)機(jī)制那樣,是一種無鎖實(shí)現(xiàn)低矮。但是印叁,STM 目前不適用于兩種場(chǎng)合 ③ :
- 大量的 I/O 阻塞
- 大量的運(yùn)算在 C Extensions 中進(jìn)行
相反 GIL 對(duì)這兩種場(chǎng)合非常在行。事實(shí)上并行 I/O 是多線程主要解決的問題军掂,并行計(jì)算在 Python 中又常常是交給 C Extensions 處理的轮蜕,所以 CPython GIL 在實(shí)際使用時(shí),在并發(fā)性能上蝗锥,并不會(huì)輸于無 GIL 的 PyPy STM跃洛。這再次證明了 GIL 是一個(gè)相當(dāng)杰出的設(shè)計(jì)。
四终议、我的方案
順便說一下汇竭,如果讓我來設(shè)計(jì) Python,要支持多線程的話穴张,我一定會(huì)采用 GIL细燎。但是多線程就一定是必須的嗎?事實(shí)上在我看來皂甘,多線程并不是一個(gè)很好的并發(fā)模型玻驻。在需要多核進(jìn)行并行計(jì)算的時(shí)候,我們可以采用更安全的多進(jìn)程模式叮贩,在需要高并發(fā) I/O 的時(shí)候击狮,我們則可以選擇更加安全、高效的“異步”和“協(xié)程”模式 —— 如果不使用多線程益老,我們自然就可以去除 GIL 了彪蓬。
所以,我啟動(dòng)了一個(gè)叫做“C10K ④ ”的項(xiàng)目捺萌,通過 DLL Injection 在不修改用戶程序的情況下档冬,將線程自動(dòng)替換成協(xié)程,這能把任意語言編寫的程序都變成異步程序桃纯,并且順便把 Python 的 GIL 給去掉了 —— 這也是我目前認(rèn)為酷誓,最漂亮的去除 GIL 的方法了。我在視頻「標(biāo)準(zhǔn)線程的協(xié)程替換 ⑤ 」中做了詳細(xì)講解态坦。
五盐数、GIL 出現(xiàn)的歷史原因
Python 開始于 1989年 12月,為 Amoeba 操作系統(tǒng)(一種分布式 POSIX 系統(tǒng))設(shè)計(jì)伞梯,當(dāng)時(shí)是小型機(jī)時(shí)代玫氢,而 PC 機(jī)則處于 386/486 時(shí)代帚屉。雖然在 90 年代我在國(guó)內(nèi)的科研單位已經(jīng)看到有堆疊了大量 CPU 的工程機(jī)(今天叫做眾核機(jī)),但是“真正意義上的”多核(對(duì)稱雙 CPU 方案漾峡,SMP)實(shí)際商用的產(chǎn)品 ⑥ 出現(xiàn)于 1999 年攻旦。而最早的單一多核 CPU ⑦ 出現(xiàn)于 2000~2001 年。也就是說生逸,在 Python 出現(xiàn)的那個(gè)時(shí)代牢屋,多核尚未實(shí)質(zhì)性出現(xiàn)。
既然沒有多核槽袄,那么也就不存在“基于多核多線”的“并行計(jì)算”了烙无。由于當(dāng)時(shí) Unix 程序員習(xí)慣上是使用多進(jìn)程來處理多任務(wù)的(進(jìn)程在 Unix 中很輕,尤其是內(nèi)存在進(jìn)程間是通過“寫復(fù)制”來共享的)掰伸,所以多線程在單核時(shí)代的“唯一用途”是配合多進(jìn)程皱炉,在進(jìn)程中并行化處理阻塞式 I/O 用的怀估。
由于多線程會(huì)對(duì) CPU 產(chǎn)生競(jìng)爭(zhēng)狮鸭,在單核時(shí)代,線程開得越多多搀,系統(tǒng)性能就會(huì)越低歧蕉。所以在 Python 出現(xiàn)的那個(gè)年代,如果要提升程序性能康铭,正確的做法并不是去增加活躍的線程數(shù)量惯退,相反應(yīng)該把活躍線程數(shù)減少,以降低 CPU 競(jìng)爭(zhēng)从藤。既然在“單核時(shí)代”線程并沒有“多核并行計(jì)算”的用途催跪,僅僅是處理阻塞式 I/O 用的,那么提升性能最好的方法就是:把整個(gè)程序鎖住夷野,讓程序中只能有一個(gè)活躍線程 —— 這就是 GIL懊蒸。
最后我用一個(gè)極端假設(shè)來說明這個(gè)問題:“假設(shè)可以在沒有任何性能損耗的情況下去掉 Python 的 GIL”—— 那么在單核時(shí)代,GIL CPython 還是會(huì)比 GIL-free CPython 性能更高 —— 所以站在當(dāng)時(shí)的角度來看悯搔,只能說 GIL 干得漂亮骑丸。
六、GIL 的真正問題
GIL 并不影響 I/O 并行妒貌,在需要多核并行計(jì)算的時(shí)候通危,CPython 會(huì)通過 C Extensions 和多進(jìn)程來解決問題,因此 GIL 對(duì)并行計(jì)算的影響其實(shí)經(jīng)常不大灌曙。多進(jìn)程的 IPC 損耗也沒有那么泛化菊碟,主要是集中在與共享內(nèi)存相關(guān)的 ⑴ 高頻內(nèi)存共享 ⑵ 大內(nèi)存共享 —— 這兩個(gè)場(chǎng)景中。這時(shí)經(jīng)過權(quán)衡在刺,GIL 對(duì)性能的影響“也許”會(huì)超過多進(jìn)程操作臨界資源所帶來的安全性好處逆害,這最終會(huì)影響到程序開發(fā)的自由度藏古。GIL 阻礙了程序的“按需并行 ⑧ ” —— 這才是更本質(zhì)的問題。
① 整理自知乎上的探討 https://www.zhihu.com/question/439920631/answer/1685766305
② It isn't Easy to Remove the GIL https://www.artima.com/weblogs/viewpost.jsp?thread=214235
③ Software Transactional Memory https://doc.pypy.org/en/latest/stm.html
④ C10K Plan https://github.com/wilhelmshen/c10k
⑤ 沈崴 - 標(biāo)準(zhǔn)線程的協(xié)程替換 - PyCon China 2020 視頻版 https://www.bilibili.com/video/BV1ir4y1c7Dw 文字版 http://www.reibang.com/p/c3e1b60d8eaf
⑥ ABIT BP6 https://en.wikipedia.org/wiki/ABIT_BP6
⑦ Power4 The First Multi-Core, 1GHz Processor https://www.ibm.com/ibm/history/ibm100/us/en/icons/power4/
⑧ Why Is GIL Worse Than We Thought? https://laike9m.com/blog/why-is-gil-worse-than-we-thought,140/