我們已經(jīng)聽說過全局解釋器鎖(GIL),擔(dān)心會影響到多線程的性能。盡管Python完全支持多線程編程,但是在解釋器的C語言實現(xiàn)中,有一部分并不是線程安全的,因此不能完全支持并發(fā)執(zhí)行。
事實上,解釋器被一個稱為全局解釋器鎖的東西保護(hù)著,在任意時刻只允許一個Python線程投入執(zhí)行。GIL帶來的最明顯的影響就是多線程的python程序無法充分利用多個CPU核心帶來的優(yōu)勢(即,一個采用多線程技術(shù)的計算密集型應(yīng)用只能在一個CPU上運行)。
在討論規(guī)避GIL的常用方案之前,需要重點強(qiáng)調(diào)的是,GIL只會對CPU密集型的程序產(chǎn)生影響(主要完成計算任務(wù)的程序)。如果我們的程序主要是在做I/O操作,比如處理網(wǎng)絡(luò)連接,那么選擇多線程技術(shù)常常是一個明智的選擇。因為他們大部分時間都花在等待對放發(fā)起連接上了。實際上可以創(chuàng)建數(shù)以千計的Python線程都沒問題。在現(xiàn)代的計算機(jī)上運行這么多線程是不會有問題的。
在理解這部分的內(nèi)容的時候,你可以將存在全局解釋器鎖的Python解釋器看作一個加油站。

I/O密集型任務(wù)中,你可以理解為有多輛小轎車駛?cè)肓思佑驼具M(jìn)行加油,雖然不知道哪個先加完,但是會比只有一個加油點的加油站順序很快。如果是計算密集型的任務(wù)呢,就是當(dāng)這輛車駛?cè)爰佑驼镜臅r候,加油站所有的資源都用來服務(wù)這一輛車了,別的車只能等待。
當(dāng)然上述比喻只是為了大家方便理解兩種不同類型任務(wù)的區(qū)別。
對于CPU密集型的程序,我們需要對問題的本質(zhì)做些研究,例如,仔細(xì)選擇底層使用的算法,這可能會比嘗試將一個沒有優(yōu)化過的算法用多線程來并行處理所帶來的性能提升要多得多。同樣的,由于Python是解釋型語言,往往只需要簡單的將性能關(guān)鍵的代碼轉(zhuǎn)移到C語言擴(kuò)展的模塊中就可能得到極大的性能提升。類似NumPy這樣的擴(kuò)展模塊對于加速涉及數(shù)組數(shù)據(jù)的特定計算也是非常高效的。最后但同樣重要的是,還可以嘗試使用其他的解釋器實現(xiàn),比如說使用了JIT編譯優(yōu)化技術(shù)的PYPY。
同樣值得指出的是,使用多線程技術(shù)并不是為了獲得性能的提升。一個CPU密集型的程序可能會用多線程來管理圖形用戶界面、網(wǎng)絡(luò)連接或者其他類型的服務(wù)。在這種情況下GIL實際上會帶來更多的問題。因為如果某部分代碼持有GIL鎖的時間過長,那就會導(dǎo)致其他非CPU密集型的線程都阻塞住。實際上,一個寫的很糟糕的C擴(kuò)展模塊會讓這個問題更加嚴(yán)重,盡管代碼中C實現(xiàn)的部分會比之前運行的快。
說這么多,要規(guī)避GIL的限制主要有兩種常用的策略。
-
如果完全使用Python來編程,可以使用
multiprocessin模塊來創(chuàng)建進(jìn)程池,把它當(dāng)作協(xié)處理器來使用??纯聪旅娴睦樱?/p># 大量的計算任務(wù) (CPU bound) def some_work(args): # ... result = args return result # 線程函數(shù) def some_thread(): while True: # ... args = None r = some_work(args) # ...上面是使用線程去處理這個任務(wù),下面我們可以將代碼改為進(jìn)程池的方式:
pool = None # 大量的計算任務(wù) (CPU bound) def some_work(args): # ... result = args return result # 線程函數(shù) def some_thread(): while True: # ... r = pool.apply(some_work, (args)) # ... if __name__ == "__main__": import multiprocessing pool = multiprocessing.Pool()使用進(jìn)程池的例子通過一個巧妙的辦法避開了GIL的限制。每當(dāng)有線程要執(zhí)行CPU密集型任務(wù)的時候,就把任務(wù)提交到進(jìn)程池中,然后進(jìn)程池將任務(wù)轉(zhuǎn)交給運行在另一個進(jìn)程中的python解釋器。當(dāng)線程等待結(jié)果的時候就會釋放GIL。此外,由于計算是在另一個單獨的解釋器中進(jìn)行的,這就不再受到GIL的限制了,在多核系統(tǒng)上,將會發(fā)現(xiàn)采用這種技術(shù)能夠輕易利用到所有的CPU核心。
第二種方式是把重點放在C語言的擴(kuò)展編程上。主要思想就是將計算密集的任務(wù)轉(zhuǎn)移到C語言中,使其獨立于Python,在C代碼中釋放GIL,這里由于我也不會C,就不做示例了。
在我們面對多線程程序性能問題的時候,不能去抱怨GIL是所有問題的根源。但是,這么做只是一種短視和幼稚的行為。舉個例子,在多線程網(wǎng)絡(luò)程序中出現(xiàn)神秘的“僵死”現(xiàn)象,這種現(xiàn)象可能是別的原因造成的,和GIL沒有一點關(guān)系。所以要先認(rèn)真研究自己的代碼,判斷GIL是否才是問題的關(guān)鍵。CPU密集型的處理才是需要考慮GIL,I/O密集的處理則不必要考慮。