Python中的GIL

GIL 的全稱為 Global Interpreter Lock ,意即全局解釋器鎖。在 Python 語言的主流實現(xiàn) CPython 中,GIL 是一個貨真價實的全局線程鎖,在解釋器解釋執(zhí)行任何 Python 代碼時,都需要先獲得這把鎖才行,在遇到 I/O 操作時會釋放這把鎖。如果是純計算的程序,沒有 I/O 操作,解釋器會每隔 100 次操作就釋放這把鎖,讓別的線程有機(jī)會執(zhí)行(這個次數(shù)可以通過sys.setcheckinterval 來調(diào)整)。所以雖然 CPython 的線程庫直接封裝操作系統(tǒng)的原生線程,但 CPython 進(jìn)程做為一個整體,同一時間只會有一個獲得了 GIL 的線程在跑,其它的線程都處于等待狀態(tài)等著 GIL 的釋放。這也就解釋了我們上面的實驗結(jié)果:雖然有兩個死循環(huán)的線程,而且有兩個物理 CPU 內(nèi)核,但因為 GIL 的限制,兩個線程只是做著分時切換,總的 CPU 占用率還略低于 50%。

看起來 python 很不給力啊。GIL 直接導(dǎo)致 CPython 不能利用物理多核的性能加速運(yùn)算。那為什么會有這樣的設(shè)計呢?我猜想應(yīng)該還是歷史遺留問題。多核 CPU 在 1990 年代還屬于類科幻,Guido van Rossum 在創(chuàng)造 python 的時候,也想不到他的語言有一天會被用到很可能 1000+ 個核的 CPU 上面,一個全局鎖搞定多線程安全在那個時代應(yīng)該是最簡單經(jīng)濟(jì)的設(shè)計了。簡單而又能滿足需求,那就是合適的設(shè)計(對設(shè)計來說,應(yīng)該只有合適與否,而沒有好與不好)。怪只怪硬件的發(fā)展實在太快了,摩爾定律給軟件業(yè)的紅利這么快就要到頭了。短短 20 年不到,代碼工人就不能指望僅僅靠升級 CPU 就能讓老軟件跑的更快了。在多核時代,編程的免費(fèi)午餐沒有了。如果程序不能用并發(fā)擠干每個核的運(yùn)算性能,那就意謂著會被淘汰。對軟件如此,對語言也是一樣。那 Python 的對策呢?

Python 的應(yīng)對很簡單,以不變應(yīng)萬變。在最新的 python 3 中依然有 GIL。之所以不去掉,原因嘛,不外以下幾點(diǎn):

欲練神功,揮刀自宮:

CPython 的 GIL 本意是用來保護(hù)所有全局的解釋器和環(huán)境狀態(tài)變量的。如果去掉 GIL,就需要多個更細(xì)粒度的鎖對解釋器的眾多全局狀態(tài)進(jìn)行保護(hù)?;蛘卟捎?Lock-Free 算法。無論哪一種,要做到多線程安全都會比單使用 GIL 一個鎖要難的多。而且改動的對象還是有 20 年歷史的 CPython 代碼樹,更不論有這么多第三方的擴(kuò)展也在依賴 GIL。對 Python 社區(qū)來說,這不異于揮刀自宮,重新來過。

就算自宮,也未必成功:

有位牛人曾經(jīng)做了一個驗證用的 CPython,將 GIL 去掉,加入了更多的細(xì)粒度鎖。但是經(jīng)過實際的測試,對單線程程序來說,這個版本有很大的性能下降,只有在利用的物理 CPU 超過一定數(shù)目后,才會比 GIL 版本的性能好。這也難怪。單線程本來就不需要什么鎖。單就鎖管理本身來說,鎖 GIL 這個粗粒度的鎖肯定比管理眾多細(xì)粒度的鎖要快的多。而現(xiàn)在絕大部分的 python 程序都是單線程的。再者,從需求來說,使用 python 絕不是因為看中它的運(yùn)算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。費(fèi)了大力氣把 GIL 拿掉,反而讓大部分的程序都變慢了,這不是南轅北轍嗎。

難道 Python 這么優(yōu)秀的語言真的僅僅因為改動困難和意義不大就放棄多核時代了嗎?其實,不做改動最最重要的原因還在于:不用自宮,也一樣能成功!

多進(jìn)程

那除了切掉 GIL 外,果然還有方法讓 Python 在多核時代活的滋潤?讓我們回到本文最初的那個問題:如何能讓這個死循環(huán)的 Python 腳本在雙核機(jī)器上占用 100% 的 CPU?其實最簡單的答案應(yīng)該是:運(yùn)行兩個 python 死循環(huán)的程序!也就是說,用兩個分別占滿一個 CPU 內(nèi)核的 python 進(jìn)程來做到。確實,多進(jìn)程也是利用多個 CPU 的好方法。只是進(jìn)程間內(nèi)存地址空間獨(dú)立,互相協(xié)同通信要比多線程麻煩很多。有感于此,Python 在3.6里新引入了 multiprocessing這個多進(jìn)程標(biāo)準(zhǔn)庫,讓多進(jìn)程的 python 程序編寫簡化到類似多線程的程度,大大減輕了 GIL 帶來的不能利用多核的尷尬。

C擴(kuò)展

這還只是一個方法,如果不想用多進(jìn)程這樣重量級的解決方案,還有個更徹底的方案,放棄 Python,改用 C/C++。當(dāng)然,你也不用做的這么絕,只需要把關(guān)鍵部分用 C/C++ 寫成 Python 擴(kuò)展,其它部分還是用 Python 來寫,讓 Python 的歸 Python,C 的歸 C。一般計算密集性的程序都會用 C 代碼編寫并通過擴(kuò)展的方式集成到 Python 腳本里(如 NumPy 模塊)。在擴(kuò)展里就完全可以用 C 創(chuàng)建原生線程,而且不用鎖 GIL,充分利用 CPU 的計算資源了。不過,寫 Python 擴(kuò)展總是讓人覺得很復(fù)雜。好在 Python 還有另一種與 C 模塊進(jìn)行互通的機(jī)制: ctypes

利用 ctypes 繞過GIL

ctypes 與 Python 擴(kuò)展不同,它可以讓 Python 直接調(diào)用任意的 C 動態(tài)庫的導(dǎo)出函數(shù)。你所要做的只是用 ctypes 寫些 python 代碼即可。最酷的是,ctypes 會在調(diào)用 C 函數(shù)前釋放 GIL。所以,我們可以通過 ctypes 和 C 動態(tài)庫來讓 python 充分利用物理內(nèi)核的計算能力。讓我們來實際驗證一下,這次我們用 C 寫一個死循環(huán)函數(shù)

extern"C"

{

? void DeadLoop()

? {

??? while (true);

? }

}

用上面的 C 代碼編譯生成動態(tài)庫 libdead_loop.so (Windows 上是 dead_loop.dll)

,接著就要利用 ctypes 來在 python 里 load 這個動態(tài)庫,分別在主線程和新建線程里調(diào)用其中的DeadLoop

from ctypes import *

from threading importThread


lib = cdll.LoadLibrary("libdead_loop.so")

t = Thread(target=lib.DeadLoop)

t.start()


lib.DeadLoop()

這回再看看 system monitor,Python 解釋器進(jìn)程有兩個線程在跑,而且雙核 CPU 全被占滿了,ctypes 確實很給力!需要提醒的是,GIL 是被 ctypes 在調(diào)用 C 函數(shù)前釋放的。但是 Python 解釋器還是會在執(zhí)行任意一段 Python 代碼時鎖 GIL 的。如果你使用 Python 的代碼做為 C 函數(shù)的 callback,那么只要 Python 的 callback 方法被執(zhí)行時,GIL 還是會跳出來的。比如下面的例子:

extern"C"

{

? typedef void Callback();

? void Call(Callback* callback)

? {

??? callback();

? }

}

from ctypes import *

from threading importThread


def dead_loop():

??? while True:

??????? pass

lib = cdll.LoadLibrary("libcall.so")

Callback = CFUNCTYPE(None)

callback = Callback(dead_loop)


t = Thread(target=lib.Call, args=(callback,))

t.start()

lib.Call(callback)

注意這里與上個例子的不同之處,這次的死循環(huán)是發(fā)生在 Python 代碼里 (DeadLoop 函數(shù)) 而 C 代碼只是負(fù)責(zé)去調(diào)用這個 callback 而已。運(yùn)行這個例子,你會發(fā)現(xiàn) CPU 占用率還是只有 50% 不到。GIL 又起作用了。

其實,從上面的例子,我們還能看出 ctypes 的一個應(yīng)用,那就是用 Python 寫自動化測試用例,通過 ctypes 直接調(diào)用 C 模塊的接口來對這個模塊進(jìn)行黑盒測試,哪怕是有關(guān)該模塊 C 接口的多線程安全方面的測試,ctypes 也一樣能做到。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • GIL 是什么東西?它對我們的 python 程序會產(chǎn)生什么樣的影響?我們先來看一個問題。運(yùn)行下面這段 pytho...
    忘了呼吸的那只貓閱讀 370評論 0 1
  • 熟悉Python的人對GIL肯定都不陌生, 其全稱是全局解釋器鎖(Global Interpreter Lock)...
    小蛋子閱讀 553評論 0 2
  • 必備的理論基礎(chǔ) 1.操作系統(tǒng)作用: 隱藏丑陋復(fù)雜的硬件接口,提供良好的抽象接口。 管理調(diào)度進(jìn)程,并將多個進(jìn)程對硬件...
    drfung閱讀 3,747評論 0 5
  • GIL 是什么東西?它對我們的 python 程序會產(chǎn)生什么樣的影響? 我們先來看一個問題。運(yùn)行下面這段 pyth...
    lintong閱讀 1,554評論 0 32
  • 中午的外教課上,外教老師問我一個問題“你認(rèn)為成為一名老師所應(yīng)該具備的基本素養(yǎng)是什么?”我脫口而出”耐心”。這好像...
    菩提無茵閱讀 1,190評論 0 1

友情鏈接更多精彩內(nèi)容