本篇文章咱們來探討一下併發設計模型。java
可使用不一樣的併發模型來實現併發系統,併發模型說的是系統中的線程如何協做完成併發任務。不一樣的併發模型以不一樣的方式拆分任務,線程能夠以不一樣的方式進行通訊和協做。程序員
併發模型其實和分佈式系統模型很是類似,在併發模型中是線程
彼此進行通訊,而在分佈式系統模型中是 進程
彼此進行通訊。然而本質上,進程和線程也很是類似。這也就是爲何併發模型和分佈式模型很是類似的緣由。算法
分佈式系統一般要比並發系統面臨更多的挑戰和問題好比進程通訊、網絡可能出現異常,或者遠程機器掛掉等等。可是一個併發模型一樣面臨着好比 CPU 故障、網卡出現問題、硬盤出現問題等。數據庫
由於併發模型和分佈式模型很類似,所以他們能夠相互借鑑,例如用於線程分配的模型就相似於分佈式系統環境中的負載均衡模型。編程
其實說白了,分佈式模型的思想就是借鑑併發模型的基礎上推演發展來的。數組
併發模型的一個重要的方面是,線程是否應該共享狀態
,是具備共享狀態
仍是獨立狀態
。共享狀態也就意味着在不一樣線程之間共享某些狀態緩存
狀態其實就是數據
,好比一個或者多個對象。當線程要共享數據時,就會形成 競態條件
或者 死鎖
等問題。固然,這些問題只是可能會出現,具體實現方式取決於你是否安全的使用和訪問共享對象。安全
獨立的狀態代表狀態不會在多個線程之間共享,若是線程之間須要通訊的話,他們能夠訪問不可變的對象來實現,這是一種最有效的避免併發問題的一種方式,以下圖所示網絡
使用獨立狀態讓咱們的設計更加簡單,由於只有一個線程可以訪問對象,即便交換對象,也是不可變的對象。數據結構
第一個併發模型是並行 worker 模型,客戶端會把任務交給 代理人(Delegator)
,而後由代理人把工做分配給不一樣的 工人(worker)
。以下圖所示
並行 worker 的核心思想是,它主要有兩個進程即代理人和工人,Delegator 負責接收來自客戶端的任務並把任務下發,交給具體的 Worker 進行處理,Worker 處理完成後把結果返回給 Delegator,在 Delegator 接收到 Worker 處理的結果後對其進行彙總,而後交給客戶端。
並行 Worker 模型是 Java 併發模型中很是常見的一種模型。許多 java.util.concurrent
包下的併發工具都使用了這種模型。
並行 Worker 模型的一個很是明顯的特色就是很容易理解,爲了提升系統的並行度你能夠增長多個 Worker 完成任務。
並行 Worker 模型的另一個好處就是,它會將一個任務拆分紅多個小任務,併發執行,Delegator 在接受到 Worker 的處理結果後就會返回給 Client,整個 Worker -> Delegator -> Client 的過程是異步
的。
一樣的,並行 Worker 模式一樣會有一些隱藏的缺點
共享狀態會變得很複雜
實際的並行 Worker 要比咱們圖中畫出的更復雜,主要是並行 Worker 一般會訪問內存或共享數據庫中的某些共享數據。
這些共享狀態可能會使用一些工做隊列來保存業務數據、數據緩存、數據庫的鏈接池等。在線程通訊中,線程須要確保共享狀態是否可以讓其餘線程共享,而不是僅僅停留在 CPU 緩存中讓本身可用,固然這些都是程序員在設計時就須要考慮的問題。線程須要避免 競態條件
,死鎖
和許多其餘共享狀態形成的併發問題。
多線程在訪問共享數據時,會丟失併發性,由於操做系統要保證只有一個線程可以訪問數據,這會致使共享數據的爭用和搶佔。未搶佔到資源的線程會 阻塞
。
現代的非阻塞併發算法能夠減小爭用提升性能,可是非阻塞算法比較難以實現。
可持久化的數據結構(Persistent data structures)
是另一個選擇。可持久化的數據結構在修改後始終會保留先前版本。所以,若是多個線程同時修改一個可持久化的數據結構,而且一個線程對其進行了修改,則修改的線程會得到對新數據結構的引用。
雖然可持久化的數據結構是一個新的解決方法,可是這種方法實行起來卻有一些問題,好比,一個持久列表會將新元素添加到列表的開頭,並返回所添加的新元素的引用,可是其餘線程仍然只持有列表中先前的第一個元素的引用,他們看不到新添加的元素。
持久化的數據結構好比 鏈表(LinkedList)
在硬件性能上表現不佳。列表中的每一個元素都是一個對象,這些對象散佈在計算機內存中。現代 CPU 的順序訪問每每要快的多,所以使用數組等順序訪問的數據結構則可以得到更高的性能。CPU 高速緩存能夠將一個大的矩陣塊加載到高速緩存中,並讓 CPU 在加載後直接訪問 CPU 高速緩存中的數據。對於鏈表,將元素分散在整個 RAM 上,這其實是不可能的。
無狀態的 worker
共享狀態能夠由其餘線程所修改,所以,worker 必須在每次操做共享狀態時從新讀取,以確保在副本上可以正確工做。不在線程內部保持狀態的 worker 成爲無狀態的 worker。
做業順序是不肯定的
並行工做模型的另外一個缺點是做業的順序不肯定,沒法保證首先執行或最後執行哪些做業。任務 A 在任務 B 以前分配給 worker,可是任務 B 可能在任務 A 以前執行。
第二種併發模型就是咱們常常在生產車間遇到的 流水線併發模型
,下面是流水線設計模型的流程圖
這種組織架構就像是工廠中裝配線中的 worker,每一個 worker 只完成所有工做的一部分,完成一部分後,worker 會將工做轉發給下一個 worker。
每道程序都在本身的線程中運行,彼此之間不會共享狀態,這種模型也被稱爲無共享併發模型。
使用流水線併發模型一般被設計爲非阻塞I/O
,也就是說,當沒有給 worker 分配任務時,worker 會作其餘工做。非阻塞I/O 意味着當 worker 開始 I/O 操做,例如從網絡中讀取文件,worker 不會等待 I/O 調用完成。由於 I/O 操做很慢,因此等待 I/O 很是耗費時間。在等待 I/O 的同時,CPU 能夠作其餘事情,I/O 操做完成後的結果將傳遞給下一個 worker。下面是非阻塞 I/O 的流程圖
在實際狀況中,任務一般不會按着一條裝配線流動,因爲大多數程序須要作不少事情,所以須要根據完成的不一樣工做在不一樣的 worker 之間流動,以下圖所示
任務還可能須要多個 worker 共同參與完成
使用流水線模型的系統有時也被稱爲 響應式
或者 事件驅動系統
,這種模型會根據外部的事件做出響應,事件多是某個 HTTP 請求或者某個文件完成加載到內存中。
在 Actor 模型中,每個 Actor 其實就是一個 Worker, 每個 Actor 都可以處理任務。
簡單來講,Actor 模型是一個併發模型,它定義了一系列系統組件應該如何動做和交互的通用規則,最著名的使用這套規則的編程語言是 Erlang。一個參與者Actor
對接收到的消息作出響應,而後能夠建立出更多的 Actor 或發送更多的消息,同時準備接收下一條消息。
在 Channel 模型中,worker 一般不會直接通訊,與此相對的,他們一般將事件發送到不一樣的 通道(Channel)
上,而後其餘 worker 能夠在這些通道上獲取消息,下面是 Channel 的模型圖
有的時候 worker 不須要明確知道接下來的 worker 是誰,他們只須要將做者寫入通道中,監聽 Channel 的 worker 能夠訂閱或者取消訂閱,這種方式下降了 worker 和 worker 之間的耦合性。
與並行設計模型相比,流水線模型具備一些優點,具體優點以下
不會存在共享狀態
由於流水線設計可以保證 worker 在處理完成後再傳遞給下一個 worker,因此 worker 與 worker 之間不須要共享任何狀態,也就不用無需考慮覺得併發而引發的併發問題。你甚至能夠在實現上把每一個 worker 當作是單線程的一種。
有狀態 worker
由於 worker 知道沒有其餘線程修改自身的數據,因此流水線設計中的 worker 是有狀態的,有狀態的意思是他們能夠將須要操做的數據保留在內存中,有狀態一般比無狀態更快。
更好的硬件整合
由於你能夠把流水線當作是單線程的,而單線程的工做優點在於它可以和硬件的工做方式相同。由於有狀態的 worker 一般在 CPU 中緩存數據,這樣能夠更快地訪問緩存的數據。
使任務更加有效的進行
能夠對流水線併發模型中的任務進行排序,通常用來日誌的寫入和恢復。
流水線併發模型的缺點是任務會涉及多個 worker,所以可能會分散在項目代碼的多個類中。所以很難肯定每一個 worker 都在執行哪一個任務。流水線的代碼編寫也比較困難,設計許多嵌套回調處理程序的代碼一般被稱爲 回調地獄
。回調地獄很難追蹤 debug。
函數性並行模型是最近才提出的一種併發模型,它的基本思路是使用函數調用來實現。消息的傳遞就至關因而函數的調用。傳遞給函數的參數都會被拷貝,所以在函數以外的任何實體都沒法操縱函數內的數據。這使得函數執行相似於原子
操做。每一個函數調用均可以獨立於任何其餘函數調用執行。
當每一個函數調用獨立執行時,每一個函數均可以在單獨的 CPU 上執行。這也就是說,函數式並行並行至關因而各個 CPU 單獨執行各自的任務。
JDK 1.7 中的 ForkAndJoinPool
類就實現了函數性並行的功能。Java 8 提出了 stream 的概念,使用並行流也可以實現大量集合的迭代。
函數性並行的難點是要知道函數的調用流程以及哪些 CPU 執行了哪些函數,跨 CPU 函數調用會帶來額外的開銷。
做者:cxuan本文版權歸做者和博客園共有,未經做者容許不能轉載,不然追究法律責任的權利。 若是文中有什麼錯誤,歡迎指出。以避免更多的人被誤導。 |