做者:不洗碗工做室 - Markluxnode
出處:Marklux's Pub程序員
版權歸做者全部,轉載請註明出處編程
因爲接觸過的語言比較多,各類語言之間對併發任務的處理方式不盡相同,時常會使人迷惑。此次嘗試系統的整理和總結一下常見的併發調度方式和編程模型。緩存
《併發之痛》中認爲,併發編程最難解決的問題是,究竟要建立多少個線程(這裏指內核線程)才最合適。不一樣的語言對於這個問題的理解和解決方案都不一樣,但大體能夠從用戶線程和內核線程的比值上作一個分類多線程
參考下圖:併發
一個使用編程語言建立的線程完整的映射到一個內核線程上去,Java就是這種實現。這麼作線程的調度將主要依賴於操做系統自己的調度,若是建立的線程比較多,可能會由於上下文的切換問題致使性能低下。框架
這麼作的好處在於,開發人員至關於直接在控制內核線程。經過合理的編程(如使用線程池),理論上能夠最大限度的利用CPU性能。異步
缺點則是線程的建立和銷燬成本比較高,同時編程模型也相對複雜一些,很差掌握。編程語言
便是說,用戶線程能夠映射到任意一個內核線程上去。這種方式最爲靈活,但編程語言自己的調度器實現難度會比較複雜一些,並且開發人員在開發時也就沒法再直接去調度內核線程了。Go應該是目前爲止實現M:N映射的語言中性能最好的一個。分佈式
Go的映射方案以下圖:M爲系統線程,P爲調度器,G爲routine也就是用戶線程。
當內核線程數量 = CPU核心數的時候,這種映射模型能夠很輕鬆的使程序的CPU利用率達到一個很高的水平。
優點就是用戶線程能夠變的很輕量,建立銷燬和調度的成本都很低,大部分狀況下也沒必要要刻意去維護線程池來提高性能了,編程難度下降了很多。
至於缺點就仁者見仁智者見智了,Go的調度器性能並非最優的,也不能保證全部場景下的高效率,遇到這類場合可能還要在代碼上下一番功夫。
以下圖
嚴格意義上這種映射不算完整的併發,由於沒法徹底利用多核的性能優點,並且任務也不能實現並行。
在腳本語言中用的比較多,如今最出名的應該就是JS了,並且恰恰使用了單線程的node性能還不差,所以如今也有很多人推崇這套方案。
編程模型上基本所有須要異步來實現,若是對異步編程方法瞭解的不夠透徹寫起來可能會比較頭疼。
引入多線程後,線程和線程之間的資源競爭和執行順序依賴都成爲使人頭疼的問題,這時就必須讓線程之間能夠互相通訊才能解決,而通訊機制的編程模型不一樣,也會很大程度上影響程序員編寫併發代碼時的思路。
最基本的一種解決方式,就是經過多個線程同時讀寫某個變量的方法來實現。這麼作很直觀,不過若是鎖使用不當,頗有可能會形成嚴重的性能損耗,更不要說死鎖這類的狀況了。
因爲很是常見,因此不加以贅述了,主要重點放在下面兩種方式上:
傳統的鎖機制並很差駕馭,這時候CSP(Communicating Sequential Process)出現了,它的原則是「經過通訊來共享內存,而不是用共享內存來通訊」。
所以CSP模型裏把通訊方式抽象成了管道(channel),全部線程間的通訊都依賴於管道的讀寫來實現。
相比於單純的內存鎖,管道對內存的讀寫者分別提供了各自的等待隊列,而且封裝了調度的邏輯,這樣就簡化了多線程通訊的複雜度,只要合理使用channel,就能夠解決至關一部分的問題。
而go還爲channel添加了緩存空間,使得channel的通訊方式從單一的同步阻塞,變得能夠在必定數量上支持異步。
CSP最大的亮點在於只關心消息的傳輸方式,而把消息的發送者和讀寫者給解耦了,這背後蘊含的邏輯是「讀寫者很是關心本身的數據有沒有被處理」,若是不是這樣的場景,可能使用CSP模型帶來的改觀就不是很大了。
這個模型相對來講比較新穎一些。目前已知的是Scala中有較大規模的應用。
和CSP不一樣,Actor把重點放在消息的處理單元而不是消息的傳輸方式上,發送者和讀寫者都必須事先知道對方是誰,纔可以進行消息的收發。
因爲沒有完整的使用過Actor模型,在這裏引用一下別人總結的對於Actor模型的優點:
相關參考:併發之痛