併發程式設計模型小結

不洗碗工作室發表於2018-09-27

作者:不洗碗工作室 - Marklux

出處:Marklux's Pub

版權歸作者所有,轉載請註明出處

由於接觸過的語言比較多,各種語言之間對併發任務的處理方式不盡相同,時常會令人迷惑。這次嘗試系統的整理和總結一下常見的併發排程方式和程式設計模型。

使用的執行緒數

《併發之痛》中認為,併發程式設計最難解決的問題是,究竟要建立多少個執行緒(這裡指核心執行緒)才最合適。不同的語言對於這個問題的理解和解決方案都不同,但大致可以從使用者執行緒和核心執行緒的比值上做一個分類

使用者執行緒:核心執行緒 = 1:1

參考下圖:

併發程式設計模型小結

一個使用程式語言建立的執行緒完整的對映到一個核心執行緒上去,Java就是這種實現。這麼做執行緒的排程將主要依賴於作業系統本身的排程,如果建立的執行緒比較多,可能會因為上下文的切換問題導致效能低下。

這麼做的好處在於,開發人員相當於直接在控制核心執行緒。通過合理的程式設計(如使用執行緒池),理論上可以最大限度的利用CPU效能。

缺點則是執行緒的建立和銷燬成本比較高,同時程式設計模型也相對複雜一些,不好掌握。

使用者執行緒:核心執行緒 = M:N

即是說,使用者執行緒可以對映到任意一個核心執行緒上去。這種方式最為靈活,但程式語言本身的排程器實現難度會比較複雜一些,而且開發人員在開發時也就無法再直接去排程核心執行緒了。Go應該是目前為止實現M:N對映的語言中效能最好的一個。

Go的對映方案如下圖:M為系統執行緒,P為排程器,G為routine也就是使用者執行緒。

併發程式設計模型小結

當核心執行緒數量 = CPU核心數的時候,這種對映模型可以很輕鬆的使程式的CPU利用率達到一個很高的水平。

優勢就是使用者執行緒可以變的很輕量,建立銷燬和排程的成本都很低,大部分情況下也不必要刻意去維護執行緒池來提升效能了,程式設計難度降低了不少。

至於缺點就仁者見仁智者見智了,Go的排程器效能並不是最優的,也不能保證所有場景下的高效率,遇到這類場合可能還要在程式碼上下一番功夫。

使用者執行緒:核心執行緒 = N:1

如下圖

併發程式設計模型小結

嚴格意義上這種對映不算完整的併發,因為無法完全利用多核的效能優勢,而且任務也不能實現並行。

在指令碼語言中用的比較多,現在最出名的應該就是JS了,而且偏偏使用了單執行緒的node效能還不差,因此現在也有不少人推崇這套方案。

程式設計模型上基本全部需要非同步來實現,如果對非同步程式設計方法瞭解的不夠透徹寫起來可能會比較頭疼。

執行緒間通訊的方法

引入多執行緒後,執行緒和執行緒之間的資源競爭和執行順序依賴都成為令人頭疼的問題,這時就必須讓執行緒之間可以互相通訊才能解決,而通訊機制的程式設計模型不同,也會很大程度上影響程式設計師編寫併發程式碼時的思路。

Lock共享資源

最基本的一種解決方式,就是通過多個執行緒同時讀寫某個變數的方法來實現。這麼做很直觀,不過如果鎖使用不當,很有可能會造成嚴重的效能損耗,更不要說死鎖這類的情況了。

由於非常常見,所以不加以贅述了,主要重點放在下面兩種方式上:

CSP執行緒通訊

傳統的鎖機制並不好駕馭,這時候CSP(Communicating Sequential Process)出現了,它的原則是“通過通訊來共享記憶體,而不是用共享記憶體來通訊”。

因此CSP模型裡把通訊方式抽象成了管道(channel),所有執行緒間的通訊都依賴於管道的讀寫來實現。

相比於單純的記憶體鎖,管道對記憶體的讀寫者分別提供了各自的等待佇列,並且封裝了排程的邏輯,這樣就簡化了多執行緒通訊的複雜度,只要合理使用channel,就可以解決相當一部分的問題。

而go還為channel新增了快取空間,使得channel的通訊方式從單一的同步阻塞,變得可以在一定數量上支援非同步。

CSP最大的亮點在於只關心訊息的傳輸方式,而把訊息的傳送者和讀寫者給解耦了,這背後蘊含的邏輯是“讀寫者非常關心自己的資料有沒有被處理”,如果不是這樣的場景,可能使用CSP模型帶來的改觀就不是很大了。

Actor模型

這個模型相對來說比較新穎一些。目前已知的是Scala中有較大規模的應用。

和CSP不同,Actor把重點放在訊息的處理單元而不是訊息的傳輸方式上,傳送者和讀寫者都必須事先知道對方是誰,才能夠進行訊息的收發。

由於沒有完整的使用過Actor模型,在這裡引用一下別人總結的對於Actor模型的優勢:

  • Actor可獨立更新,實現熱升級。因為Actor互相之間沒有直接的耦合,是相對獨立的實體,可能實現熱升級。
  • 無縫彌合本地和遠端呼叫 因為Actor使用基於訊息的通訊機制,無論是和本地的Actor,還是遠端Actor互動,都是通過訊息,這樣就彌合了本地和遠端的差異。
  • 容錯 Actor之間的通訊是非同步的,傳送方只管傳送,不關心超時以及錯誤,這些都由框架層和獨立的錯誤處理機制接管。
  • 易擴充套件,天然分散式 因為Actor的通訊機制彌合了本地和遠端呼叫,本地Actor處理不過來的時候,可以在遠端節點上啟動Actor然後轉發訊息過去。

相關參考:併發之痛

相關文章