當前位置: 華文問答 > 數位

為什麽有人說 Python 的多執行緒是雞肋呢?

2014-04-21數位

在介紹Python中的執行緒之前,先明確一個問題,Python中的多執行緒是 假的多執行緒 ! 為什麽這麽說,我們先明確一個概念,全域直譯器鎖(GIL)。

Python程式碼的執行由Python虛擬機器(直譯器)來控制。Python在設計之初就考慮要在主迴圈中,同時只有一個執行緒在執行,就像單CPU的系統中執行多個行程那樣,記憶體中可以存放多個程式,但任意時刻,只有一個程式在CPU中執行。同樣地,雖然Python直譯器可以執行多個執行緒,只有一個執行緒在直譯器中執行。

對Python虛擬機器的存取由全域直譯器鎖(GIL)來控制,正是這個鎖能保證同時只有一個執行緒在執行。在多執行緒環境中,Python虛擬機器按照以下方式執行。

1.設定GIL。

2.切換到一個執行緒去執行。

3.執行。

4.把執行緒設定為睡眠狀態。

5.解鎖GIL。

6.再次重復以上步驟。

對所有面向I/O的(會呼叫內建的作業系統C程式碼的)程式來說,GIL會在這個I/O呼叫之前被釋放,以允許其他執行緒在這個執行緒等待I/O的時候執行。如果某執行緒並未使用很多I/O操作,它會在自己的時間片內一直占用處理器和GIL。也就是說,I/O密集型的Python程式比計算密集型的Python程式更能充分利用多執行緒的好處。

我們都知道,比方我有一個4核的CPU,那麽這樣一來,在單位時間內每個核只能跑一個執行緒,然後時間片輪轉切換。但是Python不一樣,它不管你有幾個核,單位時間多個核只能跑一個執行緒,然後時間片輪轉。看起來很不可思議?但是這就是GIL搞的鬼。任何Python執行緒執行前,必須先獲得GIL鎖,然後,每執行100條字節碼,直譯器就自動釋放GIL鎖,讓別的執行緒有機會執行。這個GIL全域鎖實際上把所有執行緒的執行程式碼都給上了鎖,所以,多執行緒在Python中只能交替執行,即使100個執行緒跑在100核CPU上,也只能用到1個核。通常我們用的直譯器是官方實作的CPython,要真正利用多核,除非重寫一個不帶GIL的直譯器。

我們不妨做個試驗:

#coding=utf-8 from multiprocessing import Pool from threading import Thread from multiprocessing import Process def loop(): while True: pass if __name__ == '__main__': for i in range(3): t = Thread(target=loop) t.start() while True: pass

我的電腦是4核,所以我開了4個執行緒,看一下CPU資源占有率:

我們發現CPU利用率並沒有占滿,大致相當於單核水平。

而如果我們變成行程呢?

我們改一下程式碼:

#coding=utf-8 from multiprocessing import Pool from threading import Thread from multiprocessing import Process def loop(): while True: pass if __name__ == '__main__': for i in range(3): t = Process(target=loop) t.start() while True: pass

結果直接飆到了100%,說明行程是可以利用多核的!

為了驗證這是Python中的GIL搞得鬼,我試著用Java寫相同的程式碼,開啟執行緒,我們觀察一下:

package com.darrenchan.thread; public class TestThread { public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { while (true) { } } }).start(); } while(true){ } } }

由此可見,Java中的多執行緒是可以利用多核的,這是真正的多執行緒!而Python中的多執行緒只能利用單核,這是假的多執行緒!

難道就如此?我們沒有辦法在Python中利用多核?當然可以!剛才的多行程算是一種解決方案,還有一種就是呼叫C語言的連結庫。對所有面向I/O的(會呼叫內建的作業系統C程式碼的)程式來說,GIL會在這個I/O呼叫之前被釋放,以允許其他執行緒在這個執行緒等待I/O的時候執行。我們可以把一些 計算密集型任務用C語言編寫,然後把.so連結庫內容載入到Python中,因為執行C程式碼,GIL鎖會釋放,這樣一來,就可以做到每個核都跑一個執行緒的目的!

可能有的小夥伴不太理解什麽是計算密集型任務,什麽是I/O密集型任務?

計算密集型任務的特點是要進行大量的計算,消耗CPU資源,比如計算圓周率、對視訊進行高畫質解碼等等,全靠CPU的運算能力。這種計算密集型任務雖然也可以用多工完成,但是任務越多,花在工作切換的時間就越多,CPU執行任務的效率就越低,所以,要最高效地利用CPU,計算密集型任務同時進行的數量應當等於CPU的核心數。

計算密集型任務由於主要消耗CPU資源,因此,程式碼執行效率至關重要。Python這樣的手稿語言執行效率很低,完全不適合計算密集型任務。對於計算密集型任務,最好用C語言編寫。

第二種任務的型別是IO密集型,涉及到網路、磁盤IO的任務都是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部份時間都在等待IO操作完成(因為IO的速度遠遠低於CPU和記憶體的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。常見的大部份任務都是IO密集型任務,比如Web套用。

IO密集型任務執行期間,99%的時間都花在IO上,花在CPU上的時間很少,因此,用執行速度極快的C語言替換用Python這樣執行速度極低的手稿語言,完全無法提升執行效率。對於IO密集型任務,最合適的語言就是開發效率最高(程式碼量最少)的語言,手稿語言是首選,C語言最差。

綜上,Python多執行緒相當於單核多執行緒,多執行緒有兩個好處:CPU並列,IO並列,單核多執行緒相當於自斷一臂。所以,在Python中,可以使用多執行緒,但不要指望能有效利用多核。如果一定要透過多執行緒利用多核,那只能透過C擴充套件來實作,不過這樣就失去了Python簡單易用的特點。不過,也不用過於擔心,Python雖然不能利用多執行緒實作多核任務,但可以透過多行程實作多核任務。多個Python行程有各自獨立的GIL鎖,互不影響。

分享廖雪峰的部落格:廖雪峰部落格