CPython 有 GIL 是因為當年設計的人偷懶嗎?

語言: CN / TW / HK

△點選上方“ Python貓 ”關注 ,回覆“ 1 ”領取電子書

花下貓語: 今天分享的文章來自國內 Python 圈的資深大佬 沈崴 老師,他從 Python 1版本起就是重度使用者。原文取自他知乎的一篇回答,為了便於閱讀,我略作了調整。沈老師也有自己的公眾號(沈崴),如需轉載,請聯絡他開白。

劇照:《眷思量》

作者:沈崴

來源: https://www.zhihu.com/question/439920631/answer/1685766305

這是一個好問題。關於這個問題,簡單的答案是: 不僅沒有偷懶,相反 GIL 是一個傑出的設計。

一、Greg Stein 的嘗試

Guido van Rossum 提到[1],在 1999 年,Greg Stein(及 Mark Hammond ?)曾嘗試開發過一個無 GIL 的 Python(據信是 1.5 版)分支,該分支對“所有變數”施以細粒度執行緒鎖。這讓 Python 的單執行緒效能下降了兩倍,這足以抵消多執行緒所能為 Python 帶來的效能提升。

這次嘗試讓 Python 在取消 GIL 這件事上變得非常謹慎,GIL 得以保留至今。但是反過來說,我們發現 Guido 在最初所選擇的 GIL —— 其實已經是最優方案了。

二、GIL 為什麼快?

拿資料庫來進行類比,使用粗粒度的 GIL 就好象在操作資料時,直接鎖定整個資料庫。SQLite(非 WAL 模式)就是這樣一種直接鎖庫的實現,同時 SQLite 也是在單執行緒下最快的主流 SQL 資料庫。

使用細粒度鎖來代替 GIL,類似於鎖定到每個欄位。由於過於影響效能,資料庫鎖定粒度一般不會做到這麼細的程度。而對於 Python 直譯器來說,由於很難從使用者程式碼中抽離出類似於資料庫“行、表”級別的程式碼段,來進行中等粒度的鎖定,所以也就無法做到在不大量損失效能的情況下,來去除 GIL 了。

我們在實際開發中,經常會遇到類似的粒度選擇問題。有經驗的開發者習慣上會採用一些“簡單粗暴”的方法來解決問題,雖然看上去這很初級,但是這樣做卻是最安全、穩定,和高效的。相反許多初學者反而會採用很多複雜、“高階”的方法來解決問題,這些程式碼往往都不夠安全、穩定,同時又很低效。

事實往往和表面上看到的不太一樣。對於有經驗的開發者來說,簡單粗暴的大規模資料鎖、GIL 等方案,其實是經常會被用到的。這往往是一種下意識的選擇,背後是有豐富的開發經驗來支撐的,並不是偷懶的結果。

三、PyPy 的嘗試

PyPy 使用 STM(Software Transactional Memory)來去除 GIL,這有點像 Python 的 ZODB 資料庫基於 Conflict 的併發機制那樣,是一種無鎖實現。但是,STM 目前不適用於兩種場合[2]:

  1. 大量的 I/O 阻塞

  2. 大量的運算在 C Extensions 中進行

相反 GIL 對這兩種場合非常在行。事實上並行 I/O 是多執行緒主要解決的問題,平行計算在 Python 中又常常是交給 C Extensions 處理的,所以 CPython GIL 在實際使用時,在併發效能上,並不會輸於無 GIL 的 PyPy STM。這再次證明了 GIL 是一個相當傑出的設計。

四、我的方案

順便說一下,如果讓我來設計 Python,要支援多執行緒的話,我一定會採用 GIL。但是多執行緒就一定是必須的嗎?事實上在我看來,多執行緒並不是一個很好的併發模型。在需要多核進行平行計算的時候,我們可以採用更安全的多程序模式,在需要高併發 I/O 的時候,我們則可以選擇更加安全、高效的“非同步”和“協程”模式 —— 如果不使用多執行緒,我們自然就可以去除 GIL 了。

所以,我啟動了一個叫做“C10K[3]”的專案,通過 DLL Injection 在不修改使用者程式的情況下,將執行緒自動替換成協程,這能把任意語言編寫的程式都變成非同步程式,並且順便把 Python 的 GIL 給去掉了 —— 這也是我目前認為,最漂亮的去除 GIL 的方法了。我在影片「標準執行緒的協程替換[4]」中做了詳細講解。

—— 2021 年 1月 19日

五、GIL 出現的歷史原因

Python 開始於 1989年 12月,為 Amoeba 作業系統(一種分散式 POSIX 系統)設計,當時是小型機時代,而 PC 機則處於 386/486 時代。雖然在 90 年代我在國內的科研單位已經看到有堆疊了大量 CPU 的工程機(今天叫做眾核機),但是“真正意義上的”多核(對稱雙 CPU 方案,SMP)實際商用的產品[5]出現於 1999 年。而最早的單一多核 CPU[6] 出現於 2000~2001 年。也就是說,在 Python 出現的那個時代,多核尚未實質性出現。

既然沒有多核,那麼也就不存在“基於多核多線”的“平行計算”了。由於當時 Unix 程式設計師習慣上是使用多程序來處理多工的(程序在 Unix 中很輕,尤其是記憶體在程序間是通過“寫複製”來共享的),所以多執行緒在單核時代的“唯一用途”是配合多程序,在程序中並行化處理阻塞式 I/O 用的。

由於多執行緒會對 CPU 產生競爭,在單核時代,執行緒開得越多,系統性能就會越低。所以在 Python 出現的那個年代,如果要提升程式效能,正確的做法並不是去增加活躍的執行緒數量,相反應該把活躍執行緒數減少,以降低 CPU 競爭。既然在“單核時代”執行緒並沒有“多核平行計算”的用途,僅僅是處理阻塞式 I/O 用的,那麼提升效能最好的方法就是:把整個程式鎖住,讓程式中只能有一個活躍執行緒 —— 這就是 GIL。

最後我用一個極端假設來說明這個問題:“假設可以在沒有任何效能損耗的情況下去掉 Python 的 GIL”—— 那麼在單核時代,GIL CPython 還是會比 GIL-free CPython 效能更高 —— 所以站在當時的角度來看,只能說 GIL 幹得漂亮。

六、GIL 的真正問題

GIL 並不影響 I/O 並行,在需要多核平行計算的時候,CPython 會通過 C Extensions 和多程序來解決問題,因此 GIL 對平行計算的影響其實經常不大。多程序的 IPC 損耗也沒有那麼泛化,主要是集中在與共享記憶體相關的 ⑴ 高頻記憶體共享 ⑵ 大記憶體共享 —— 這兩個場景中。這時經過權衡,GIL 對效能的影響“也許”會超過多程序操作臨界資源所帶來的安全性好處,這最終會影響到程式開發的自由度。GIL 阻礙了程式的“按需並行[7]” —— 這才是更本質的問題。

—— 2021年 1月 23日

參考

  1. It isn't Easy to Remove the GIL https://www.artima.com/weblogs/viewpost.jsp?thread=214235

  2. Software Transactional Memory https://doc.pypy.org/en/latest/stm.html

  3. C10K Plan https://github.com/wilhelmshen/c10k

  4. 沈崴 - 標準執行緒的協程替換 - PyCon China 2020 https://www.bilibili.com/video/BV1ir4y1c7Dw

  5. ABIT BP6 https://en.wikipedia.org/wiki/ABIT_BP6

  6. Power4 The First Multi-Core, 1GHz Processor https://www.ibm.com/ibm/history/ibm100/us/en/icons/power4/

  7. Why Is GIL Worse Than We Thought? https://laike9m.com/blog/why-is-gil-worse-than-we-thought,140

Python貓技術交流群開放啦! 群裡既有國內一二線大廠在職員工,也有國內外高校在讀學生,既有十多年碼齡的程式設計老鳥,也有中小學剛剛入門的新人,學習氛圍良好!想入群的同學,請在公號內回覆『 交流群 』,獲取貓哥的微信 (謝絕廣告黨,非誠勿擾!) ~

還不過癮?試試它們

Python 協程與 JavaScript 協程的對比

pypy 真的能讓 Python 比 c 還快麼?

Birdseye:極其強大的Python除錯工具!

10箇中文成語,10種Python新手錯誤

Python 之父為什麼嫌棄 lambda 匿名函式?

Flask 作者 Armin Ronacher:我不覺得有非同步壓力

如果你覺得本文有幫助

請慷慨 分享 點贊 ,感謝啦