分類目錄:《系統學習Python》總目錄
在文章《并發模型和異步編程:基礎知識》我們簡單介紹了Python中的進程、線程和協程。本文就著重介紹Python中的進程、線程和GIL的關系。
Python解釋器的每個實例都是一個進程。使用multiprocessing
或concurrent.futures
庫可以啟動額外的Python進程。Python的subprocess
庫用于啟動運行外部程序(不管使用何種語言編寫)的進程。而Python解釋器僅使用一個線程運行用戶的程序和內存垃圾回收程序。使用threading
或concurrent.futures
庫可以啟動額外的Python線程。對對象引用計數和解釋器其他內部狀態的訪問受一個鎖的控制,這個鎖就是全局解釋器鎖(Global Interpreter Lock,GIL)。任意時間點上只有一個Python線程可以持有GIL。這意味著,任意時間點上只有一個線程能執行Python代碼,與CPU核數量無關。為了防止一個Python線程無限期持有GIL,Python的字節碼解釋器默認每5毫秒暫停當前Python線程,釋放GIL。被暫停的線程可以再次嘗試獲得GIL,但是如果有其他線程等待,那么操作系統調度程序可能會從中挑選一個線程開展工作。我們編寫的Python代碼無法控制GIL。但是,耗時的任務可由內置函數或C語言(以及其他能在Python/C API層級接合的語言)擴展釋放GIL。Python標準庫中發起系統調用的函數均可釋放GIL。這包括所有執行磁盤I/O、網絡I/O的函數,以及time.sleep()
。NumPy
/SciPy
庫中很多CPU密集型函數,以及zlib
和bz2
模塊中執行壓縮和解壓操作的函數,也都釋放GIL。在Python/C API層級集成的擴展也可以啟動不受GIL影響的非Python線程。這些不受GIL影響的線程無法更改Python對象,但是可以讀取或寫入內存中支持緩沖協議的底層對象,例如bytearray
、array.array
和NumPy
數組。
GIL對使用Python線程進行網絡編程的影響相對較小,因為I/O函數釋放GIL,而且與內存讀寫相比,網絡讀寫的延遲始終很高。各個單獨的線程無論如何都要花費大量時間等待,所以線程可以交錯執行,對整體吞吐量不會產生重大影響。正如David Beazley所言:?“Python線程非常擅長什么都不做。?對GIL的爭用會降低計算密集型Python線程的速度。對于這類任務,在單線程中依序執行的代碼更簡單,速度也更快。若想在多核上運行CPU密集型Python代碼,必須使用多個Python進程。threading
模塊的文檔對此做了很好的概括。
由于CPython有GIL,因此同一時間只有一個線程能執行Python代碼(盡管有些旨在提升性能的庫可以克服這個限制)?。如果我們希望應用程序充分地利用多核設備的計算資源,那么建議使用multiprocessing
或concurrent.futures.ProcessPoolExecutor
。然而,如果我們想同時運行多個I/O密集型任務,那么線程仍是最合適的模型。前一段開頭指出那是“CPython實現細節”?,因為GIL不是Python語言規定的機制。Jython和IronPython就沒有GIL。可惜,二者落后較多,還停留在Python2.7。高性能的PyPy解釋器的2.7和3.7版本也有GIL。
本文沒有提到協程,因為默認情況下,協程共用同一個Python線程,而且受異步框架提供的事件循環監管,所以不受GIL影響。在異步程序中也可以使用多個線程,但是最佳實踐是在同一個線程中運行事件循環和所有協程,其他線程負責執行特定的任務。
參考文獻:
[1] Mark Lutz. Python學習手冊[M]. 機械工業出版社, 2018.
[2] 盧西亞諾·拉馬略.流暢的Python 第2版(全2冊) 編程語言[M].人民郵電出版社,2023.