[譯] Python 3 中的執行緒,GIL,執行緒安全(上)
假期看了一篇關於 Python3 執行緒的文章(https://lemanchet.fr/articles/learning-python-3-threading-module.html),感覺非常棒,特意分享給大家。說它好在於完完全全解答了我的很多疑問,以一種更高階的方式解讀 Python3 中的執行緒,尤其現在大家都在說非同步程式設計,很容易線上程和非同步之間做比較,而這篇文章無疑解釋的很好。
另外讀完這篇文章也在想,為什麼大家有這麼多錯誤的理解?難道深入一門語言一定要研究語言的原始碼嗎?我們如何才能更加高效的掌握一個技能?
這篇文章從三部分解釋了執行緒,分別是原理、實踐、解疑,為了把事情說清楚,我分兩篇文章說明,關於程式碼實踐部分在下一篇介紹。
什麼是執行緒?
在過去,執行緒認為是一種輕量級的任務,比如主程式在後臺啟動五個執行緒,在需要的時候可以將五個執行緒執行的結果彙總在一起。
當啟動python程式的時候,會產生一個包含主執行緒的新程序,如果你需要,可以並行執行多個任務(即執行緒)。
一個常見的例子就是主執行緒等待網路連線,然後將接收到的連線交給其他執行緒去處理。
執行緒由作業系統管理,在大多數情況下,程式呼叫 pthreads C 庫讓作業系統來建立新程序,CPython 也是如此工作的。
可怕的執行緒
執行緒非常強大但也很難使用:因為它們共享相同的記憶體空間。
正因為共享,所以執行緒非常的輕量,產生一個執行緒僅僅額外需要一點點記憶體,核心有足夠的記憶體處理執行緒棧。
這也意味著同個程序的每個執行緒能夠互相訪問其他執行緒。比如一個執行緒處理一個網路套接字,並不能保證僅僅由它來處理,在同一時刻,其他執行緒也能處理該套接字,比如修改、關閉、銷燬。
很多和執行緒打交道的程式設計師都有一個共識:使用執行緒編寫程式碼很難。
執行緒安全
執行緒編碼很困難,但已經存在很長時間了,很多程式設計師傾向使用它,所以必須儘可能找到一種方式和它共存。
一些優秀的開發者發明了很多可以和執行緒良好共存的可重用的工具和庫,這些 API 可以避免執行緒陷阱,這些 API 是執行緒安全的。
在 Python 中,執行緒安全通常通過避免共享可變狀態來實現,重點就是避免修改執行緒共享的資料。
當然完全避免共享可變狀態是不可能的,執行緒在協作的時候必須:
-
所有的執行緒可以讀,完全沒有執行緒在寫入資料。
-
一個單獨的執行緒在寫,沒有其他執行緒在讀。
如果要理解執行緒,這兩點必須要記住。
Rust在編譯程式碼的時候能夠保證程式碼是執行緒安全的,如果你經常編寫與執行緒有關的程式碼,可以採用它。
使用 Python 編寫執行緒程式碼的難點在於你無法證明程式碼是執行緒安全的,就算有單元測試、靜態分析器也沒有用,唯一要做的就是小心編寫程式碼。
CPython,GIL,原子操作
網路上有很多對 Python 執行緒的誤解,最糟糕也是最流行的錯誤觀點是“Python不能使用執行緒”,如果你不使用執行緒,那麼你就不會犯錯,這是你不使用 Python 執行緒的原因嗎?
另外一種沒有危險的說法:由於 GIL 的存在,能夠保證 Python 執行緒編碼是執行緒安全的。
GIL 是由 CPython 實現的,GIL 能夠避免 Python 內部出現和執行緒安全有關的一些錯誤(並不是為了執行緒安全而產生的 )。
1:Python 絕對能使用執行緒,不要有絲毫疑問
CPython 使用作業系統原生的執行緒,GIL 能夠確保同一時刻僅有一個執行緒執行 Python 位元組碼,潛臺詞是 CPython 無法有效使用多核。
這個特性讓 Python 很適合有大量 IO 操作的任務(這些任務不依賴於多核 CPU)。
如果是 CPU 密集操作,即使你有多核,使用 CPython 多執行緒編碼是很糟糕的,如果要發揮多核的能力,為規避 CPython GIL 帶來的問題,有以下兩種解決方案:
-
用原生 C 程式碼編寫,某些庫(比如 Numpy)不會有 GIL 帶來的問題。
-
用多程序代替多執行緒。
2:GIL 能讓 Python 程式碼執行緒安全嗎?
不會,GIL 不會讓編寫的 Python 程式碼執行緒安全,但會保證某些 Python 基本操作原子性。其他一些 Python 實現,比如 Pypy 不會有相同的保證。
重要的是,GIL 並不完全保證 Python 的基本操作是原子性的,帶來的影響就是很難校驗程式碼是執行緒安全的。
一些常見的問題
1:執行緒安全的代價
如果你編寫多執行緒程式碼,額外的付出就是記憶體的消耗,具體消耗依賴很多因素,每個執行緒至少消耗 32KB。
當作業系統決定切換執行緒的時候,上下文切換也是要付出 CPU 時間的,和其他 Python 操作相比,這些消耗可以忽略不計。一個相對較新的作業系統很容易同時處理1萬個執行緒。
執行緒需要有很多的開發成本,多執行緒程式開發比單執行緒程式開發有更多的複雜性。
2:多執行緒還是非同步IO
關於這個問題很容易迷失,有很多非同步IO庫,但 Python 生態系統仍然在尋找社群都滿意的非同步 IO 庫,這些第三方庫多多少少存在一些問題。
換句話說,第三方庫視同步的執行緒程式碼為一等公民,如果你忘記非同步 IO,會從這種開發模型中獲益。
對於開發人員來說也是如此,寫同步程式碼是相對容易的,每個開發者都知道這一點,而想找到在有經驗的非同步編碼的人則相對較難。
3:多執行緒有沒有落伍?
現在如果你關心一些流行的開發語言,很容易會這麼想(比如我),比如 Nodejs 僅使用單執行緒執行時間迴圈;Go 語言對開發者隱藏了多執行緒,提供一種更簡單的介面進行併發程式設計。
但多執行緒仍然是很多工首選的解決方案,比如檔案系統的操作;Linux 並沒有提供非同步 API;甚至很多重度依賴非同步 IO 的軟體也會使用多執行緒,比如 Nginx 也會使用執行緒池避免非同步操作變慢。
值得一提的是 CPU 的頻率增速已經逐步停止了,現在是多核的時代,要麼使用多執行緒,要麼使用多程序。
歡迎關注我的公眾號(ID:yudadanwx,虞大膽的嘰嘰喳喳)和書《深入淺出HTTPS:從原理到實戰》 。