我們已經(jīng)聽說過全局解釋器鎖(GIL)哩掺,擔(dān)心會(huì)影響到多線程的性能垄懂。盡管Python完全支持多線程編程虚汛,但是在解釋器的C語言實(shí)現(xiàn)中围来,有一部分并不是線程安全的跺涤,因此不能完全支持并發(fā)執(zhí)行。
事實(shí)上管钳,解釋器被一個(gè)稱為全局解釋器鎖的東西保護(hù)著钦铁,在任意時(shí)刻只允許一個(gè)Python線程投入執(zhí)行。GIL帶來的最明顯的影響就是多線程的python程序無法充分利用多個(gè)CPU核心帶來的優(yōu)勢(shì)(即才漆,一個(gè)采用多線程技術(shù)的計(jì)算密集型應(yīng)用只能在一個(gè)CPU上運(yùn)行)牛曹。
在討論規(guī)避GIL的常用方案之前,需要重點(diǎn)強(qiáng)調(diào)的是醇滥,GIL只會(huì)對(duì)CPU密集型的程序產(chǎn)生影響(主要完成計(jì)算任務(wù)的程序)黎比。如果我們的程序主要是在做I/O操作,比如處理網(wǎng)絡(luò)連接鸳玩,那么選擇多線程技術(shù)常常是一個(gè)明智的選擇阅虫。因?yàn)樗麄兇蟛糠謺r(shí)間都花在等待對(duì)放發(fā)起連接上了。實(shí)際上可以創(chuàng)建數(shù)以千計(jì)的Python線程都沒問題不跟。在現(xiàn)代的計(jì)算機(jī)上運(yùn)行這么多線程是不會(huì)有問題的颓帝。
在理解這部分的內(nèi)容的時(shí)候,你可以將存在全局解釋器鎖的Python解釋器看作一個(gè)加油站窝革。
I/O密集型任務(wù)中购城,你可以理解為有多輛小轎車駛?cè)肓思佑驼具M(jìn)行加油,雖然不知道哪個(gè)先加完虐译,但是會(huì)比只有一個(gè)加油點(diǎn)的加油站順序很快瘪板。如果是計(jì)算密集型的任務(wù)呢,就是當(dāng)這輛車駛?cè)爰佑驼镜臅r(shí)候漆诽,加油站所有的資源都用來服務(wù)這一輛車了侮攀,別的車只能等待锣枝。
當(dāng)然上述比喻只是為了大家方便理解兩種不同類型任務(wù)的區(qū)別。
對(duì)于CPU密集型的程序兰英,我們需要對(duì)問題的本質(zhì)做些研究撇叁,例如,仔細(xì)選擇底層使用的算法箭昵,這可能會(huì)比嘗試將一個(gè)沒有優(yōu)化過的算法用多線程來并行處理所帶來的性能提升要多得多税朴。同樣的,由于Python是解釋型語言家制,往往只需要簡(jiǎn)單的將性能關(guān)鍵的代碼轉(zhuǎn)移到C語言擴(kuò)展的模塊中就可能得到極大的性能提升。類似NumPy這樣的擴(kuò)展模塊對(duì)于加速涉及數(shù)組數(shù)據(jù)的特定計(jì)算也是非常高效的泡一。最后但同樣重要的是颤殴,還可以嘗試使用其他的解釋器實(shí)現(xiàn),比如說使用了JIT編譯優(yōu)化技術(shù)的PYPY鼻忠。
同樣值得指出的是涵但,使用多線程技術(shù)并不是為了獲得性能的提升。一個(gè)CPU密集型的程序可能會(huì)用多線程來管理圖形用戶界面帖蔓、網(wǎng)絡(luò)連接或者其他類型的服務(wù)矮瘟。在這種情況下GIL實(shí)際上會(huì)帶來更多的問題。因?yàn)槿绻巢糠执a持有GIL鎖的時(shí)間過長(zhǎng)塑娇,那就會(huì)導(dǎo)致其他非CPU密集型的線程都阻塞住澈侠。實(shí)際上埋酬,一個(gè)寫的很糟糕的C擴(kuò)展模塊會(huì)讓這個(gè)問題更加嚴(yán)重,盡管代碼中C實(shí)現(xiàn)的部分會(huì)比之前運(yùn)行的快写妥。
說這么多,要規(guī)避GIL的限制主要有兩種常用的策略珍特。
-
如果完全使用Python來編程祝峻,可以使用
multiprocessin
模塊來創(chuàng)建進(jìn)程池,把它當(dāng)作協(xié)處理器來使用扎筒。看看下面的例子:# 大量的計(jì)算任務(wù) (CPU bound) def some_work(args): # ... result = args return result # 線程函數(shù) def some_thread(): while True: # ... args = None r = some_work(args) # ...
上面是使用線程去處理這個(gè)任務(wù)砸琅,下面我們可以將代碼改為進(jìn)程池的方式:
pool = None # 大量的計(jì)算任務(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)程池的例子通過一個(gè)巧妙的辦法避開了GIL的限制。每當(dāng)有線程要執(zhí)行CPU密集型任務(wù)的時(shí)候症脂,就把任務(wù)提交到進(jìn)程池中谚赎,然后進(jìn)程池將任務(wù)轉(zhuǎn)交給運(yùn)行在另一個(gè)進(jìn)程中的python解釋器淫僻。當(dāng)線程等待結(jié)果的時(shí)候就會(huì)釋放GIL。此外壶唤,由于計(jì)算是在另一個(gè)單獨(dú)的解釋器中進(jìn)行的,這就不再受到GIL的限制了闸盔,在多核系統(tǒng)上,將會(huì)發(fā)現(xiàn)采用這種技術(shù)能夠輕易利用到所有的CPU核心迎吵。
第二種方式是把重點(diǎn)放在C語言的擴(kuò)展編程上。主要思想就是將計(jì)算密集的任務(wù)轉(zhuǎn)移到C語言中击费,使其獨(dú)立于Python,在C代碼中釋放GIL蔫巩,這里由于我也不會(huì)C,就不做示例了圆仔。
在我們面對(duì)多線程程序性能問題的時(shí)候,不能去抱怨GIL是所有問題的根源坪郭。但是,這么做只是一種短視和幼稚的行為截粗。舉個(gè)例子,在多線程網(wǎng)絡(luò)程序中出現(xiàn)神秘的“僵死”現(xiàn)象绸罗,這種現(xiàn)象可能是別的原因造成的,和GIL沒有一點(diǎn)關(guān)系珊蟀。所以要先認(rèn)真研究自己的代碼,判斷GIL是否才是問題的關(guān)鍵育灸。CPU密集型的處理才是需要考慮GIL,I/O密集的處理則不必要考慮磅崭。