原文連接http://www.cnblogs.com/stubborn412/p/4033651.htmlhtml
GIL 是什麼東西?它對咱們的 python 程序會產生什麼樣的影響?咱們先來看一個問題。運行下面這段 python 程序,CPU 佔用率是多少?python
# 請勿在工做中模仿,危險:) def dead_loop(): while True: pass dead_loop()
答案是什麼呢,佔用 100% CPU?那是單核!還得是沒有超線程的古董 CPU。在個人雙核 CPU 上,這個死循環只會吃掉我一個核的工做負荷,也就是隻佔用 50% CPU。那如何能讓它在雙核機器上佔用 100% 的 CPU 呢?答案很容易想到,用兩個線程就好了,線程不正是併發分享 CPU 運算資源的嗎。惋惜答案雖然對了,但作起來可沒那麼簡單。下面的程序在主線程以外又起了一個死循環的線程算法
import threading def dead_loop(): while True: pass # 新起一個死循環線程 t = threading.Thread(target=dead_loop) t.start() # 主線程也進入死循環 dead_loop() t.join()
按道理它應該能作到佔用兩個核的 CPU 資源,但是實際運行狀況倒是沒有什麼改變,仍是隻佔了 50% CPU 不到。這又是爲何呢?難道 python 線程不是操做系統的原生線程?打開 system monitor 一探究竟,這個佔了 50% 的 python 進程確實是有兩個線程在跑。那這兩個死循環的線程爲什麼不能佔滿雙核 CPU 資源呢?其實幕後的黑手就是 GIL。編程
GIL 的全稱爲 Global Interpreter Lock ,意即全局解釋器鎖。在 Python 語言的主流實現 CPython 中,GIL 是一個貨真價實的全局線程鎖,在解釋器解釋執行任何 Python 代碼時,都須要先得到這把鎖才行,在遇到 I/O 操做時會釋放這把鎖。若是是純計算的程序,沒有 I/O 操做,解釋器會每隔 100 次操做就釋放這把鎖,讓別的線程有機會執行(這個次數能夠經過sys.setcheckinterval 來調整)。因此雖然 CPython 的線程庫直接封裝操做系統的原生線程,但 CPython 進程作爲一個總體,同一時間只會有一個得到了 GIL 的線程在跑,其它的線程都處於等待狀態等着 GIL 的釋放。這也就解釋了咱們上面的實驗結果:雖然有兩個死循環的線程,並且有兩個物理 CPU 內核,但由於 GIL 的限制,兩個線程只是作着分時切換,總的 CPU 佔用率還略低於 50%。安全
看起來 python 很不給力啊。GIL 直接致使 CPython 不能利用物理多核的性能加速運算。那爲何會有這樣的設計呢?我猜測應該仍是歷史遺留問題。多核 CPU 在 1990 年代還屬於類科幻,Guido van Rossum 在創造 python 的時候,也想不到他的語言有一天會被用到極可能 1000+ 個核的 CPU 上面,一個全局鎖搞定多線程安全在那個時代應該是最簡單經濟的設計了。簡單而又能知足需求,那就是合適的設計(對設計來講,應該只有合適與否,而沒有好與很差)。怪只怪硬件的發展實在太快了,摩爾定律給軟件業的紅利這麼快就要到頭了。短短 20 年不到,代碼工人就不能期望僅僅靠升級 CPU 就能讓老軟件跑的更快了。在多核時代,編程的免費午飯沒有了。若是程序不能用併發擠幹每一個核的運算性能,那就意謂着會被淘汰。對軟件如此,對語言也是同樣。那 Python 的對策呢?多線程
Python 的應對很簡單,以不變應萬變。在最新的 python 3 中依然有 GIL。之因此不去掉,緣由嘛,不外如下幾點:併發
欲練神功,揮刀自宮:函數
CPython 的 GIL 本意是用來保護全部全局的解釋器和環境狀態變量的。若是去掉 GIL,就須要多個更細粒度的鎖對解釋器的衆多全局狀態進行保護。或者採用 Lock-Free 算法。不管哪種,要作到多線程安全都會比單使用 GIL 一個鎖要難的多。並且改動的對象仍是有 20 年曆史的 CPython 代碼樹,更不論有這麼多第三方的擴展也在依賴 GIL。對 Python 社區來講,這不異於揮刀自宮,從新來過。oop
就算自宮,也未必成功:性能
有位牛人曾經作了一個驗證用的 CPython,將 GIL 去掉,加入了更多的細粒度鎖。可是通過實際的測試,對單線程程序來講,這個版本有很大的性能降低,只有在利用的物理 CPU 超過必定數目後,纔會比 GIL 版本的性能好。這也難怪。單線程原本就不須要什麼鎖。單就鎖管理自己來講,鎖 GIL 這個粗粒度的鎖確定比管理衆多細粒度的鎖要快的多。而如今絕大部分的 python 程序都是單線程的。再者,從需求來講,使用 python 毫不是由於看中它的運算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。費了大力氣把 GIL 拿掉,反而讓大部分的程序都變慢了,這不是南轅北轍嗎。
難道 Python 這麼優秀的語言真的僅僅由於改動困難和意義不大就放棄多核時代了嗎?其實,不作改動最最重要的緣由還在於:不用自宮,也同樣能成功!
那除了切掉 GIL 外,果真還有方法讓 Python 在多核時代活的滋潤?讓咱們回到本文最初的那個問題:如何能讓這個死循環的 Python 腳本在雙核機器上佔用 100% 的 CPU?其實最簡單的答案應該是:運行兩個 python 死循環的程序!也就是說,用兩個分別佔滿一個 CPU 內核的 python 進程來作到。確實,多進程也是利用多個 CPU 的好方法。只是進程間內存地址空間獨立,互相協同通訊要比多線程麻煩不少。有感於此,Python 在 2.6 裏新引入了 multiprocessing這個多進程標準庫,讓多進程的 python 程序編寫簡化到相似多線程的程度,大大減輕了 GIL 帶來的不能利用多核的尷尬。
這還只是一個方法,若是不想用多進程這樣重量級的解決方案,還有個更完全的方案,放棄 Python,改用 C/C++。固然,你也不用作的這麼絕,只須要把關鍵部分用 C/C++ 寫成 Python 擴展,其它部分仍是用 Python 來寫,讓 Python 的歸 Python,C 的歸 C。通常計算密集性的程序都會用 C 代碼編寫並經過擴展的方式集成到 Python 腳本里(如 NumPy 模塊)。在擴展裏就徹底能夠用 C 建立原生線程,並且不用鎖 GIL,充分利用 CPU 的計算資源了。不過,寫 Python 擴展老是讓人以爲很複雜。好在 Python 還有另外一種與 C 模塊進行互通的機制 : ctypes
ctypes 與 Python 擴展不一樣,它可讓 Python 直接調用任意的 C 動態庫的導出函數。你所要作的只是用 ctypes 寫些 python 代碼便可。最酷的是,ctypes 會在調用 C 函數前釋放 GIL。因此,咱們能夠經過 ctypes 和 C 動態庫來讓 python 充分利用物理內核的計算能力。讓咱們來實際驗證一下,此次咱們用 C 寫一個死循環函數
extern"C" { void DeadLoop() { while (true); } }
用上面的 C 代碼編譯生成動態庫 libdead_loop.so (Windows 上是 dead_loop.dll)
,接着就要利用 ctypes 來在 python 裏 load 這個動態庫,分別在主線程和新建線程裏調用其中的 DeadLoop
from ctypes import * from threading import Thread lib = cdll.LoadLibrary("libdead_loop.so") t = Thread(target=lib.DeadLoop) t.start() lib.DeadLoop()
這回再看看 system monitor,Python 解釋器進程有兩個線程在跑,並且雙核 CPU 全被佔滿了,ctypes 確實很給力!須要提醒的是,GIL 是被 ctypes 在調用 C 函數前釋放的。可是 Python 解釋器仍是會在執行任意一段 Python 代碼時鎖 GIL 的。若是你使用 Python 的代碼作爲 C 函數的 callback,那麼只要 Python 的 callback 方法被執行時,GIL 仍是會跳出來的。好比下面的例子:
extern"C" { typedef void Callback(); void Call(Callback* callback) { callback(); } }
from ctypes import * from threading import Thread 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)
注意這裏與上個例子的不一樣之處,此次的死循環是發生在 Python 代碼裏 (DeadLoop 函數) 而 C 代碼只是負責去調用這個 callback 而已。運行這個例子,你會發現 CPU 佔用率仍是隻有 50% 不到。GIL 又起做用了。
其實,從上面的例子,咱們還能看出 ctypes 的一個應用,那就是用 Python 寫自動化測試用例,經過 ctypes 直接調用 C 模塊的接口來對這個模塊進行黑盒測試,哪怕是有關該模塊 C 接口的多線程安全方面的測試,ctypes 也同樣能作到。
雖然 CPython 的線程庫封裝了操做系統的原生線程,但卻由於 GIL 的存在致使多線程不能利用多個 CPU 內核的計算能力。好在如今 Python 有了易經筋(multiprocessing), 吸星大法(C 語言擴展機制)和獨孤九劍(ctypes),足以應付多核時代的挑戰,GIL 切仍是不切已經不重要了,不是嗎。