併發系統能夠採用多種併發編程模型來實現。併發模型指定了系統中的線程如何經過協做來完成分配給它們的做業。不一樣的併發模型採用不一樣的方式拆分做業,同時線程間的協做和交互方式也不相同。這篇併發模型教程將會較深刻地介紹目前(2015年,本文撰寫時間)比較流行的幾種併發模型。
html
本文所描述的併發模型相似於分佈式系統中使用的不少體系結構。在併發系統中線程之間能夠相互通訊。在分佈式系統中進程之間也能夠相互通訊(進程有可能在不一樣的機器中)。線程和進程之間具備不少類似的特性。這也就是爲何不少併發模型一般相似於各類分佈式系統架構。java
固然,分佈式系統在處理網絡失效、遠程主機或進程宕掉等方面也面臨着額外的挑戰。可是運行在巨型服務器上的併發系統也可能遇到相似的問題,好比一塊CPU失效、一塊網卡失效或一個磁盤損壞等狀況。雖然出現失效的機率可能很低,可是在理論上仍然有可能發生。web
因爲併發模型相似於分佈式系統架構,所以它們一般能夠互相借鑑思想。例如,爲工做者們(線程)分配做業的模型通常與分佈式系統中的負載均衡系統比較類似。一樣,它們在日誌記錄、失效轉移、冪等性等錯誤處理技術上也具備類似性。
【注:冪等性,一個冪等操做的特色是其任意屢次執行所產生的影響均與一次執行的影響相同】算法
第一種併發模型就是我所說的並行工做者模型。傳入的做業會被分配到不一樣的工做者上。下圖展現了並行工做者模型:數據庫
在並行工做者模型中,委派者(Delegator)將傳入的做業分配給不一樣的工做者。每一個工做者完成整個任務。工做者們並行運做在不一樣的線程上,甚至可能在不一樣的CPU上。編程
若是在某個汽車廠裏實現了並行工做者模型,每臺車都會由一個工人來生產。工人們將拿到汽車的生產規格,而且從頭至尾負責全部工做。數組
在Java應用系統中,並行工做者模型是最多見的併發模型(即便正在轉變)。java.util.concurrent包中的許多併發實用工具都是設計用於這個模型的。你也能夠在Java企業級(J2EE)應用服務器的設計中看到這個模型的蹤影。緩存
並行工做者模式的優勢是,它很容易理解。你只需添加更多的工做者來提升系統的並行度。服務器
例如,若是你正在作一個網絡爬蟲,能夠試試使用不一樣數量的工做者抓取到必定數量的頁面,而後看看多少數量的工做者消耗的時間最短(意味着性能最高)。因爲網絡爬蟲是一個IO密集型工做,最終結果頗有多是你電腦中的每一個CPU或核心分配了幾個線程。每一個CPU若只分配一個線程可能有點少,由於在等待數據下載的過程當中CPU將會空閒大量時間。網絡
並行工做者模型雖然看起來簡單,卻隱藏着一些缺點。接下來的章節中我會分析一些最明顯的弱點。
在實際應用中,並行工做者模型可能比前面所描述的狀況要複雜得多。共享的工做者常常須要訪問一些共享數據,不管是內存中的或者共享的數據庫中的。下圖展現了並行工做者模型是如何變得複雜的:
有些共享狀態是在像做業隊列這樣的通訊機制下。但也有一些共享狀態是業務數據,數據緩存,數據庫鏈接池等。
一旦共享狀態潛入到並行工做者模型中,將會使狀況變得複雜起來。線程須要以某種方式存取共享數據,以確保某個線程的修改可以對其餘線程可見(數據修改須要同步到主存中,不只僅將數據保存在執行這個線程的CPU的緩存中)。線程須要避免竟態,死鎖以及不少其餘共享狀態的併發性問題。
此外,在等待訪問共享數據結構時,線程之間的互相等待將會丟失部分並行性。許多併發數據結構是阻塞的,意味着在任何一個時間只有一個或者不多的線程可以訪問。這樣會致使在這些共享數據結構上出現競爭狀態。在執行須要訪問共享數據結構部分的代碼時,高競爭基本上會致使執行時出現必定程度的串行化。
如今的非阻塞併發算法也許能夠下降競爭並提高性能,可是非阻塞算法的實現比較困難。
可持久化的數據結構是另外一種選擇。在修改的時候,可持久化的數據結構老是保護它的前一個版本不受影響。所以,若是多個線程指向同一個可持久化的數據結構,而且其中一個線程進行了修改,進行修改的線程會得到一個指向新結構的引用。全部其餘線程保持對舊結構的引用,舊結構沒有被修改而且所以保證一致性。Scala編程包含幾個持久化數據結構。
【注:這裏的可持久化數據結構不是指持久化存儲,而是一種數據結構,好比Java中的String類,以及CopyOnWriteArrayList類,具體可參考】
雖然可持久化的數據結構在解決共享數據結構的併發修改時顯得很優雅,可是可持久化的數據結構的表現每每不盡人意。
好比說,一個可持久化的鏈表須要在頭部插入一個新的節點,而且返回指向這個新加入的節點的一個引用(這個節點指向了鏈表的剩餘部分)。全部其餘現場仍然保留了這個鏈表以前的第一個節點,對於這些線程來講鏈表仍然是爲改變的。它們沒法看到新加入的元素。
這種可持久化的列表採用鏈表來實現。不幸的是鏈表在現代硬件上表現的不太好。鏈表中得每一個元素都是一個獨立的對象,這些對象能夠遍及在整個計算機內存中。現代CPU可以更快的進行順序訪問,因此你能夠在現代的硬件上用數組實現的列表,以得到更高的性能。數組能夠順序的保存數據。CPU緩存可以一次加載數組的一大塊進行緩存,一旦加載完成CPU就能夠直接訪問緩存中的數據。這對於元素散落在RAM中的鏈表來講,不太可能作獲得。
共享狀態可以被系統中得其餘線程修改。因此工做者在每次須要的時候必須重讀狀態,以確保每次都能訪問到最新的副本,無論共享狀態是保存在內存中的仍是在外部數據庫中。工做者沒法在內部保存這個狀態(可是每次須要的時候能夠重讀)稱爲無狀態的。
每次都重讀須要的數據,將會致使速度變慢,特別是狀態保存在外部數據庫中的時候。
並行工做者模式的另外一個缺點是,做業執行順序是不肯定的。沒法保證哪一個做業最早或者最後被執行。做業A可能在做業B以前就被分配工做者了,可是做業B反而有可能在做業A以前執行。
並行工做者模式的這種非肯定性的特性,使得很難在任何特定的時間點推斷系統的狀態。這也使得它也更難(若是不是不可能的話)保證一個做業在其餘做業以前被執行。
第二種併發模型咱們稱之爲流水線併發模型。我之因此選用這個名字,只是爲了配合「並行工做者」的隱喻。其餘開發者可能會根據平臺或社區選擇其餘稱呼(好比說反應器系統,或事件驅動系統)。下圖表示一個流水線併發模型:
相似於工廠中生產線上的工人們那樣組織工做者。每一個工做者只負責做業中的部分工做。當完成了本身的這部分工做時工做者會將做業轉發給下一個工做者。每一個工做者在本身的線程中運行,而且不會和其餘工做者共享狀態。有時也被成爲無共享並行模型。
一般使用非阻塞的IO來設計使用流水線併發模型的系統。非阻塞IO意味着,一旦某個工做者開始一個IO操做的時候(好比讀取文件或從網絡鏈接中讀取數據),這個工做者不會一直等待IO操做的結束。IO操做速度很慢,因此等待IO操做結束很浪費CPU時間。此時CPU能夠作一些其餘事情。當IO操做完成的時候,IO操做的結果(好比讀出的數據或者數據寫完的狀態)被傳遞給下一個工做者。
有了非阻塞IO,就可使用IO操做肯定工做者之間的邊界。工做者會盡量多運行直到遇到並啓動一個IO操做。而後交出做業的控制權。當IO操做完成的時候,在流水線上的下一個工做者繼續進行操做,直到它也遇到並啓動一個IO操做。
在實際應用中,做業有可能不會沿着單一流水線進行。因爲大多數系統能夠執行多個做業,做業從一個工做者流向另外一個工做者取決於做業須要作的工做。在實際中可能會有多個不一樣的虛擬流水線同時運行。這是現實當中做業在流水線系統中可能的移動狀況:
做業甚至也有可能被轉發到超過一個工做者上併發處理。好比說,做業有可能被同時轉發到做業執行器和做業日誌器。下圖說明了三條流水線是如何經過將做業轉發給同一個工做者(中間流水線的最後一個工做者)來完成做業:
流水線有時候比這個狀況更加複雜。
採用流水線併發模型的系統有時候也稱爲反應器系統或事件驅動系統。系統內的工做者對系統內出現的事件作出反應,這些事件也有可能來自於外部世界或者發自其餘工做者。事件能夠是傳入的HTTP請求,也能夠是某個文件成功加載到內存中等。在寫這篇文章的時候,已經有不少有趣的反應器/事件驅動平臺可使用了,而且不久的未來會有更多。比較流行的彷佛是這幾個:
我我的以爲Vert.x是至關有趣的(特別是對於我這樣使用Java/JVM的人來講)
Actors 和 channels 是兩種比較相似的流水線(或反應器/事件驅動)模型。
在Actor模型中每一個工做者被稱爲actor。Actor之間能夠直接異步地發送和處理消息。Actor能夠被用來實現一個或多個像前文描述的那樣的做業處理流水線。下圖給出了Actor模型:
而在Channel模型中,工做者之間不直接進行通訊。相反,它們在不一樣的通道中發佈本身的消息(事件)。其餘工做者們能夠在這些通道上監聽消息,發送者無需知道誰在監聽。下圖給出了Channel模型:
在寫這篇文章的時候,channel模型對於我來講彷佛更加靈活。一個工做者無需知道誰在後面的流水線上處理做業。只需知道做業(或消息等)須要轉發給哪一個通道。通道上的監聽者能夠隨意訂閱或者取消訂閱,並不會影響向這個通道發送消息的工做者。這使得工做者之間具備鬆散的耦合。
相比並行工做者模型,流水線併發模型具備幾個優勢,在接下來的章節中我會介紹幾個最大的優勢。
工做者之間無需共享狀態,意味着實現的時候無需考慮全部因併發訪問共享對象而產生的併發性問題。這使得在實現工做者的時候變得很是容易。在實現工做者的時候就好像是單個線程在處理工做-基本上是一個單線程的實現。
當工做者知道了沒有其餘線程能夠修改它們的數據,工做者能夠變成有狀態的。對於有狀態,我是指,它們能夠在內存中保存它們須要操做的數據,只需在最後將更改寫回到外部存儲系統。所以,有狀態的工做者一般比無狀態的工做者具備更高的性能。
單線程代碼在整合底層硬件的時候每每具備更好的優點。首先,當能肯定代碼只在單線程模式下執行的時候,一般可以建立更優化的數據結構和算法。
其次,像前文描述的那樣,單線程有狀態的工做者可以在內存中緩存數據。在內存中緩存數據的同時,也意味着數據頗有可能也緩存在執行這個線程的CPU的緩存中。這使得訪問緩存的數據變得更快。
我說的硬件整合是指,以某種方式編寫的代碼,使得可以天然地受益於底層硬件的工做原理。有些開發者稱之爲mechanical sympathy。我更傾向於硬件整合這個術語,由於計算機只有不多的機械部件,而且可以隱喻「更好的匹配(match better)」,相比「同情(sympathy)」這個詞在上下文中的意思,我以爲「conform」這個詞表達的很是好。固然了,這裏有點吹毛求疵了,用本身喜歡的術語就行。
基於流水線併發模型實現的併發系統,在某種程度上是有可能保證做業的順序的。做業的有序性使得它更容易地推出系統在某個特定時間點的狀態。更進一步,你能夠將全部到達的做業寫入到日誌中去。一旦這個系統的某一部分掛掉了,該日誌就能夠用來重頭開始重建系統當時的狀態。按照特定的順序將做業寫入日誌,並按這個順序做爲有保障的做業順序。下圖展現了一種可能的設計:
實現一個有保障的做業順序是不容易的,但每每是可行的。若是能夠,它將大大簡化一些任務,例如備份、數據恢復、數據複製等,這些均可以經過日誌文件來完成。
流水線併發模型最大的缺點是做業的執行每每分佈到多個工做者上,並所以分佈到項目中的多個類上。這樣致使在追蹤某個做業到底被什麼代碼執行時變得困難。
一樣,這也加大了代碼編寫的難度。有時會將工做者的代碼寫成回調處理的形式。若在代碼中嵌入過多的回調處理,每每會出現所謂的回調地獄(callback hell)現象。所謂回調地獄,就是意味着在追蹤代碼在回調過程當中到底作了什麼,以及確保每一個回調只訪問它須要的數據的時候,變得很是困難
使用並行工做者模型能夠簡化這個問題。你能夠打開工做者的代碼,從頭至尾優美的閱讀被執行的代碼。固然並行工做者模式的代碼也可能一樣分佈在不一樣的類中,但每每也可以很容易的從代碼中分析執行的順序。
第三種併發模型是函數式並行模型,這是也最近(2015)討論的比較多的一種模型。函數式並行的基本思想是採用函數調用實現程序。函數能夠看做是」代理人(agents)「或者」actor「,函數之間能夠像流水線模型(AKA 反應器或者事件驅動系統)那樣互相發送消息。某個函數調用另外一個函數,這個過程相似於消息發送。
函數都是經過拷貝來傳遞參數的,因此除了接收函數外沒有實體能夠操做數據。這對於避免共享數據的競態來講是頗有必要的。一樣也使得函數的執行相似於原子操做。每一個函數調用的執行獨立於任何其餘函數的調用。
一旦每一個函數調用均可以獨立的執行,它們就能夠分散在不一樣的CPU上執行了。這也就意味着可以在多處理器上並行的執行使用函數式實現的算法。
Java7中的java.util.concurrent包裏包含的ForkAndJoinPool可以幫助咱們實現相似於函數式並行的一些東西。而Java8中並行streams可以用來幫助咱們並行的迭代大型集合。記住有些開發者對ForkAndJoinPool進行了批判(你能夠在個人ForkAndJoinPool教程裏面看到批評的連接)。
函數式並行裏面最難的是肯定須要並行的那個函數調用。跨CPU協調函數調用須要必定的開銷。某個函數完成的工做單元須要達到某個大小以彌補這個開銷。若是函數調用做用很是小,將它並行化可能比單線程、單CPU執行還慢。
我我的認爲(可能不太正確),你可使用反應器或者事件驅動模型實現一個算法,像函數式並行那樣的方法實現工做的分解。使用事件驅動模型能夠更精確的控制如何實現並行化(個人觀點)。
此外,將任務拆分給多個CPU時協調形成的開銷,僅僅在該任務是程序當前執行的惟一任務時纔有意義。可是,若是當前系統正在執行多個其餘的任務時(好比web服務器,數據庫服務器或者不少其餘相似的系統),將單個任務進行並行化是沒有意義的。無論怎樣計算機中的其餘CPU們都在忙於處理其餘任務,沒有理由用一個慢的、函數式並行的任務去擾亂它們。使用流水線(反應器)併發模型可能會更好一點,由於它開銷更小(在單線程模式下順序執行)同時能更好的與底層硬件整合。
因此,用哪一種併發模型更好呢?
一般狀況下,這個答案取決於你的系統打算作什麼。若是你的做業自己就是並行的、獨立的而且沒有必要共享狀態,你可能會使用並行工做者模型去實現你的系統。雖然許多做業都不是天然並行和獨立的。對於這種類型的系統,我相信使用流水線併發模型可以更好的發揮它的優點,並且比並行工做者模型更有優點。
你甚至不用親自編寫全部流水線模型的基礎結構。像Vert.x這種現代化的平臺已經爲你實現了不少。我也會去爲探索如何設計個人下一個項目,使它運行在像Vert.x這樣的優秀平臺上。我感受Java EE已經沒有任何優點了。
轉自:http://ifeve.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B/