Python 併發模型

發表於2016-01-10

Python 的 Threads 、 Microthreads(Tasklets) 和 Greenlets 的區別比較

最近我注意到很多 Python 論壇上的問題在詢問關於執行緒(Threads),微執行緒(Microthread)和綠色執行緒(Greenthread)這幾個併發模型之間的具體差異是什麼。問題諸如:

  • 它們在實現上有何差異?
  • 微執行緒/綠色執行緒 也有 常規執行緒 那樣的數量上限嗎?
  • 它們每個與其它相比的優缺點是什麼?我應該使用哪一個?

執行緒

離開併發模型這個集合,讓我們先從最熟知的執行緒開始。在Python中,使用規範的Posix執行緒來實現執行緒。即是,在Python中每個執行緒對映到一個系統級的執行緒,並且系統核心察覺和負責維護這些執行緒。包括執行緒執行時的搶佔,排程執行緒的下一個時隙,也包括處理上下文交換(和CPU另外一個暫存器交換執行緒狀態等等。)

執行緒的特徵就是,你執行得越多,核心排程器在同一時間應對的任務就越多。當你有太多的執行緒時,效能就會削弱,因為每個執行緒獲得的執行時間片段,變得和執行緒間交換所需時間可以相比擬——交換開銷成為了主要的瓶頸。使用執行緒,你需要保持執行的數量在一個合理的數量,100或者更少(執行一個執行緒池是一種用法例子)

因為每個Python執行緒都被核心所對映和管理,當一個上下文交換髮生的時候,在使用者空間(大多數使用者程式花費它們時間的地方)到核心空間的來回交換中,存在額外的開銷。同樣這也是一個相對昂貴的操作,這也導致了同時執行太多執行緒的問題。

儘管執行緒被認為是輕量的,我們將會看到存在更加輕量的選擇。

微執行緒(小任務)

無棧Python專案(重點修改了Python直譯器的核心以形成相容fork的Python)以小任務的名字(在介紹其他新特性中)介紹了微執行緒。

這個專案對執行緒的處理辦法是對核心隱藏執行緒,並且由Python直譯器自身處理所有的排程和上下文切換。從歷史的觀點來看,這對一個本身不支援執行緒的作業系統來說,通過對虛擬機器或者直譯器增加執行緒支援是非常有用的。(這種方法在Solaris 作業系統上的Java 1.1曾經用過,Solaris OS那時不支援執行緒)。甚至在作業系統本身支援執行緒的情況下,這種方法也有幾個優點。即執行緒之間切換管理花費得到非常大的減少-不再執行使用者空間到核心空間切換和反向切換。無棧Python通過直譯器既處理了微執行緒排程也處理了其上下文切換。

儘管在效能方面有某些優點,無棧Python專案仍然是一個與主線程式碼無關的獨立專案。這種情況出現有幾個原因(你可以從這兒閱讀到有關它的資訊),原因之一就是其中的更改並不是很細小的,並且這種更改破壞了幾個Python擴充套件。除非你的程式碼執行在你可以控制所用直譯器的機器上,否則你就可能打算堅持使用Python的參考實現而避免使用微執行緒。不過,還有一種方法使用一般的Python直譯器獲得無棧Python的某些優點—繼續向下閱讀…

Greenlets

而對於Python直譯器,微執行緒需要較大的修改,Greenlets是微執行緒的一個分支,並且能夠通過Python擴充套件來安裝(Stackless太複雜而無法成為一個擴充套件)。Greenlets的思想實際上是從Stackless專案中提取出來的,並且保留了相似的優勢,例如相對於核心,在直譯器中管理執行緒。然而也存在一個主要的區別——Greenlets的一個例項沒有明顯的排程安排。

缺少一個排程程式意味著,你能夠完全控制一個Greenlets的例項何時轉向另一個。這就是眾所周知的協作併發模型,即是為了在不同的Greenlets的例項之間進行轉換,每個Greenlets的例項都必須自願地放棄它的執行。而對於微執行緒,當轉換(也即是建立)的時候只有非常低的開銷,因為這個原因,你可以有大量的,和執行緒相關的Greenlets的例項。

協作併發模型的一個優點就是它的確定性的本質。你非常確定地知道一個Greenlet例項在哪裡退出,另外一個在哪裡開始。這容許你在處理競態條件的時候,避免在共享資料結構上使用鎖。

如果你思考一下,你會注意到Greenlets是偽併發(即使在一個沒有GIL的直譯器中)——這就意味著不可能存在多個greenlet在執行,並且它僅僅是程式中表示流程的一種方式。使用大量的if/else模組結構和一些迴圈,可以模擬模擬Greenlet的行為,但是這種方式顯然不是非常簡潔的。

附加註釋:如果你的greenlet在轉向另一個greenlet之前,碰到了一個阻塞方法的呼叫,那麼將會發生什麼?你是程式程式將會被迫暫停,直到阻塞呼叫返回。有許多非常不錯的庫,可以幫助你逃開上述問題。如果你存在I/O阻塞(套接字和檔案)呼叫,你可以考慮使用GEvent,它提供greenlets並且能夠將I/O呼叫修改為無阻塞。更多內容請猛戳

比較

在最後一節,我提到了Greenlets是偽併發的,亦即在一個給定的時間只有一個是實際在執行的。所以這就是說與微執行緒和執行緒比較起來,Greenlets處於劣勢對嗎?好吧理論上說是的,但對Python不是。關於Python和併發的問題就是聲名狼藉的GIL(全域性直譯器鎖),這使得多於一個的執行緒/微執行緒不能同時執行。當你的程式碼執行於Python直譯器時,你無法獲得多處理器系統的優勢。所以那使Greenlets同微執行緒和常規的執行緒處於同樣的基礎之上。現在問題更多的變成了,當執行具有許多執行緒的大量程式切換時,你是否需要高效能?當你的greenlets放棄執行時,你是否準備好了動手並精確控制?Greenlets是你的解決方案。

另一方面,如果你沒有較高的效能要求,希望執行多個執行緒並讓系統為你排程它們(不要忘記你需要鎖!),可以考慮使用執行緒。儘管因為 greenlets 更輕量而具有誘惑力,但在許多情況下,它讓你感覺不出有什麼實際意義。比如我目前的工作就在使用普通的執行緒,因為執行緒和搶先式切換模型也能很好解決這個問題,而且我並不需要很高的效能。

當然,若你願意安裝一個解析器的修改版,Stackless 是可選之一,通過它,你可以象隱式排程一樣方便獲得 Greenlets 的益處。

還有一個我之前沒提到的潛在解決方案。如果你有一個多核處理器,在Python中利用它的優勢的唯一途徑是使用程式。Python對程式提供的API幾乎和執行緒API是一樣的,相似的地方結束。每個程式都由自己的Python直譯器啟動,這意味著你避免了 GIL 麻煩。想讓你的系統最大化的使用,你需要啟動與你CPU核心相同數量的程式。請注意,因為你的程式碼在不同的程式執行,他們不能訪問到對方的變數,所以一些與順序有關的通訊方法需要額外的設計(有其自身效能缺陷)。

對於Python的併發,沒有一個一刀切的選擇。應根據你的實際情況仔細推敲每一個的好處,選擇最適合你的方式。

相關文章