並發系統可以使用不同的並發模型去實現。一個並發模型指定著執行緒在系統協作中是如何完成被給與的任務。不同的並發模型使用不同的方式分解工作,以及線程之間可能以不同的方式溝通和協作。這個並發模型教程將會深入的講解在我寫的時候的使用的最廣泛的並發模型。
並發模型和分散式系統類似
#在這個文字中講到的並發模型跟在分散式系統中使用的不同的框架是類似的。在一個並發系統中不同的執行緒之間互相溝通。在一個分散式系統中不同的程序進行交流(可能是不同的電腦)。本質上線程和進程是非常相似的。這個就是為什麼不同的並發模型經常看起來跟不同的分散式框架相似的原因。
當然分散式系統也有額外的挑戰,例如網路可能故障,或遠端電腦以及進程當掉等等。但是運行在一個大的伺服器的並發系統中,如果一個CPU出現故障,網卡故障,磁碟故障等等也可能出現類似的問題。這種故障的可能性雖然可能比較低,但理論上也有可能發生。
因為並發模型跟分散式系統框架是類似的,所以他們之間常常可以藉鏡一些想法。例如,在工作者(執行緒)之間分配工作的模型跟在分散式系統中的負載平衡是類似的。處理錯誤的技術像日誌,容錯等等也是相同的。
平行工作者
第一個並發模型,我們稱之為平行工作者模型。進來的任務分配給不同的工作者。這裡有一個示意圖:
在並行工作者並發模型中,一個代理人被分配進來的工作到不同的工作者。每一個工作者完成整個的任務。整個工作者是並行工作的,運行在不同的執行緒中,以及可能是在不同的CPU中。
如果一個平行工作者模型在一個汽車工廠中被實現,每一輛車將會被一個工作者生產。這個工作者將會得到說明書去建造,並且將要建造從開始到結束的每一件事情。
這個平行工作者並發模型是在java應用中使用的最廣泛的並發模型(雖然那個正在改變)。在java.util.concurrent的Java包中的許多並發工具類別被設計使用這個模型。你也可以在Java企業級應用程式中看到這個模型的痕跡。
並行工作者的優勢
並行工作者並發模型的優點在於理解起來比較簡單。為了增加應用的平行計算,你只是需要增加更多的工作者就可以了。
例如,你正在實現一個網頁爬蟲的功能,你將會使用不同數量的工作者去抓取一定數量的頁面,以及看看哪個工作者將會花費更短的抓取時間(意味著更高的性能)。因為網頁抓取是一個IO密集型工作,你可能會以電腦上的每個CPU/核心多個執行緒結束。一個CPU一個執行緒太少了,因為它在等待資料下載的時候將會空閒很久。
並行工作者的缺點
並行工作者並發模型有一些劣勢潛伏在表面。我將會在下面的部分解釋大部分的劣勢。
共享狀態取得複雜
在現實中並行工作者並發模型比上面說明的更複雜。這個共享的工作者經常需要存取一些共享數據,或者在記憶體中或在共享的資料庫中。下面的圖顯示了並行工作者並發模型的複雜性。
這個共享狀態的一些是處在像工作佇列的通訊機制裡面。但是這個共享狀態的一些是業務數據,數據緩存,資料庫連接池等等。
只要共享狀態悄悄的進入平行工作者並發模型,它就開始變得複雜了。這個線程在某種程度上需要存取共享的資料以確保被一個線程改變的對其他線程也是可見的(推向主內存,並且不只是陷入到執行這個線程的CPU的CPU緩存)。執行緒需要避免競態條件,死鎖,以及許多其他的共享狀態並發問題。
此外,當存取共享的資料結構的時候,當執行緒之間互相等待,並行計算的部分丟失了。許多並發的資料結構正在堵塞,意味著一個或一組有限的線程集合可以在任何給予的時間訪問他們。這個就可能在這些共享的資料結構上導致競爭。高競爭將會本質上導致存取共享的資料結構程式碼部分執行的一定程度上的串行化。
現代的非堵塞的並發演算法可能會降低競爭,以及提高效能,但是非堵塞演算法很難被實現。
持久化的資料結構是另外一個可供選擇的。一個持久的資料結構當被修改的時候總是會保存它自己之前的版本。此外,如果多個執行緒指向相同的持久化資料結構,並且其中一個執行緒修改它,正在修改的這個執行緒得到一個新的結構的引用。所有其他的線程將會保持對老的結構的引用,這些老的結構仍然是沒有改變的。這個Scala程式語言包含了幾個持久的資料結構。
當持久化的資料結構對於共享的資料結構的並發修改一個優雅的簡練的解決方案的時候,持久化的資料結構不會執行的很好。
例如,一個持久的清單將會新增所有新的元素到這個清單的頭部,並且傳回對於新新增元素的一個參考(這個然後執行這個清單的剩餘部分)。所有其他的線程仍然保持一個對列表中前面第一個元素的引用,並且對於其他線程這個列表顯示未改變的。他們不能看到這個新加入的元素。
這樣的一個持久化列表作為一個鍊錶實作。不幸的是鍊錶在現代軟體中不會執行的很好。在列表中的每一個元素都是分開的對象,以及這些對象可以被傳播到所有的電腦記憶體中。當代CPU在順序的存取資料會更快的,以至於在現代硬體上在一個陣列的頂層不是列表實現將會得到一個更高的效能。一個數組順序的儲存資料。這個CPU快取可以一次載入更大的區塊進入緩存,並且在這個CPU快取一旦載入完就可以直接存取資料。這個如果用鍊錶實作是不可能的,因為在鍊錶裡面的元素將會分散到所有的RAM上。
無狀態的工作者
在系統中共享的狀態可以被其他的執行緒修改。因此工作者每次需要它的時候都要重新讀取這個狀態,去確認是否工作在最新的拷貝上。這是真的,不管這個共享狀態是在記憶體還是在外部的資料庫中。一個在內部沒有保持狀態的工作者稱之為無狀態的(但是需要每次重新讀取)。
每次都需要重新讀取資料會變慢的。尤其是如果這個狀態儲存在外部的資料庫中。
任務順序是不能確定的
另外一個平行工作者模型的缺點就是執行任務的順序是不能確定的。這裡沒有辦法保證那個任務先執行,那個任務最後才執行。任務A可能會在任務B之前給予一個工作者,然而任務B可能在任務A之前執行。
平行工作者模型自然不確定使得很難去推論在某個時間點上系統的狀態。它也會很難去保證一個任務在另一個任務之前就會發生(基本上是不可能的)。
管線(Assembly Line)
第二個並發模型,我稱之為管線並發模型。我選擇這個名字只是為了適合「並行工作者」比喻的更簡單。其他的開發者使用其他的名字(例如,反應系統,或事件驅動系統)依賴平台或社群。下面有個範例圖可以說明:
這個工作者就像是工廠裡的管線上的工人。每一個工人只是執行全部工作的一部分。當那個部分完成之後,這個工人就會把這個任務轉給下一個工人。
每一個工作者都是在自己的執行緒裡面運行,並且工作者之間沒有共享的狀態。所以這個有時候也會被提及為一個無共享的並發模型。
使用管線並發模型的系統通常會使用非堵塞IO來設計。非阻塞IO意味著當一個工作者開始一個IO操作的時候(例如讀檔案或來自網路連線的資料),這個工作者不會等到IO呼叫結束。 IO操作是慢的,以至於等待IO操作完成是浪費CPU的。這個CPU可以同時做一些其他的事情。當這個IO操作結束的時候,這個IO操作的結果(例如資料讀取或資料寫的狀態)會傳遞給另外一個工作者。
對於非阻塞IO,這個IO操作決定著工作者之間的邊界範圍。一個工作者做他能做的直到它不得不開始一個IO操作。然後它放棄控制這個任務。當這個IO操作結束的時候,這個管線上的下一個工作者繼續在這個任務上工作,直到那個也不得不開始一個IO操作等等。
實際上,這些任務不一定會流動在一個生產線上。因為大部分的系統不只是執行一個任務,工人與工人之間的任務流動就依賴需要被做的那個任務。實際上,這裡將會有多個不同的虛擬管線同時在進行中。下面的圖示就是真正的管線系統中的任務是如何流動的。
任務甚至為了並發運行可能會執行不只一個工作者。例如,一個工作可能同時指向一個任務執行者和一個任務日誌。這個圖示說明了所有的三個管線是怎樣透過把他們的任務指向相同的工作者而結束的(最後的工作者在中間的流水線上):
管線甚至可以得到的比這個更複雜的。
反應系統,事件驅動系統
使用一個管線並發模型的系統通常也稱為反應系統,事件驅動系統。這個系統的工作者會對系統中正在發生的事件作出反應,或是從外部接受或被其他的工作者發出。事件的例子可以是一個HTTP請求,或是某一個檔案結束載入到記憶體等等。
在寫的時候,這裡有許多有趣的反應/事件驅動平台可用,並且在將來會有更多的。更普遍的一些看起來如下:
Vert.x
Akka
Node.JS(JavaScript)
對我個人,我發現Vert.x更有趣(尤其是想我對於Java/JVM過時的人)
作用者VS. 通道
作用者和通道是管線(或是反應系統/事件驅動)模型中兩個相似的例子。
在作用者模型中每一個工作者都稱之為作用者。作用者可以互相彼此發送訊息。訊息被發送,然後異步的執行。作用者可以使用去實現一個或更多的任務,正如之前所描述的。這裡有一個作用者的模型:
在通道模型中,工作者不能互相之間進行通訊。代替的,他們發布他們的訊息(事件)在不同的通道中。其他的工作者可以監聽這些通道的訊息,然而發送者不會知道誰正在監聽。這裡有一個通道模型的圖示:
在寫的時候,這個通道模型對我來說看起來更有彈性。一個工作者不需要知道在管線上後來工作者要執行什麼任務。它只需要知道通道將會指向這個工作什麼(或發送訊息到哪裡)。在頻道上的監聽者可以訂閱或取消,不會影響正在寫入頻道的工作者。這就允許在工作者之間鬆散耦合。
管線的優點
這個管線並發模型相對於平行工作者模型有幾個優點。在下面的部分我將會覆蓋到最大的優勢。
沒有共享狀態
事實上,工作者不會跟其他的工作者分享狀態,這意味著他們不用考慮所有的並發問題去實現。這就使得它去實現工作者更加簡單。你實現一個工作者當如果只是一個線程執行那個工作,本質上就是一個單線程實現。
有狀態的工作者
因為工作者知道不會有其他的執行緒修改他們的數據,所以這個工作者是有狀態的。有狀態的,意味著他們可以保持需要在記憶體中操作的數據,只是把最終的改變寫回外部的儲存系統。一個有狀態的工作者因此比一個無狀態的工作者更快。
更好的硬體整合
單一執行緒的程式碼有這個優勢,它經常跟底層的硬體適配的更好。首先,當你假設程式碼可以在單執行緒模式中執行的時候,你可以創建更多最佳化的資料結構和演算法。
第二,單執行緒有狀態的工作者如同上面提到的,可以快取記憶體中的資料。當資料快取到記憶體的時候,這裡也有很高的可能性,資料也會快取在執行緒的CPU的CPU快取中。這個就使得存取資料更快。
我指的它作為硬體整合當程式碼被寫的時候,從某種程度上得益於底層硬體的工作方式。有些開發者稱之為硬體的運作方式。我更喜歡硬體整合這個術語,因為計算機有很少的機械部分,並且這個詞“sympathy”在本文中為了“適配更好”作為一個暗喻來使用的,然而我相信這個詞“conform”表達的更加合理。
不管怎麼樣,這都是吹毛求疵的。使用你喜歡的字就可以了。
有順序的任務是可能的
#根據管線並發模型實現一個並發系統從某種程度上保證任務的順序是可能的。有順序的任務使得在任何給予的時間點去推理系統的狀態更加簡單。而且,你可以寫所有進來的任務到一個日誌。這個日誌可以用來一旦系統的部分失敗,就可以從失敗的地方重建。這個任務可以用一個確定順序寫入到日誌,這個順序變成固定的任務順序。設計的圖示如下所示:
實作一個固定的任務順序當然不是簡單的,但是它是可能的。如果可以,它會很大的簡化像備份,恢復數據,複製數據等等這樣的任務。這些都可以透過日誌檔去做。
管線的劣勢
流水線並發模型的主要缺點就是任務的執行經常會傳播到多個工作者,並且會透過你項目中的多個類別。因此對於去確切的看給予的任務正在執行什麼程式碼將會變的更加困難。
寫程式碼可能也會改變的困難。工作者程式碼經常會作為回調函數來寫。伴隨著更多的巢狀的回呼函數的程式碼可能會導致一些開發者呼叫什麼回呼函數厭煩。回調地獄只是意味著更加困難的去追蹤程式碼正在做的事情,和確定每一個回調需要存取他們的資料一樣。
使用並行工作者並發模型,這個將會改變的更加簡單。你可以開啟這個工作者程式碼,並且幾乎可以從開始到結束讀取已經執行的程式碼。當然,並行工作者程式碼可能也會擴散到不同的類別中,但是執行的序列進場會更簡單的從程式碼中讀取。
功能平行性(Functional Parallelism)
#功能並行性是第三個並行性模型,這個模型在這些年被討論的非常多。
功能並行性的基本想法是使用函數呼叫實現你的程式。函數被看為「代理」或「扮演者」去互相發送訊息,就像流水線並發模型(AKA反應系統或事件驅動系統)。當一個函數呼叫另外一個的時候,那個是跟發送一個訊息是相似的。
傳遞給函數的所有參數是被拷貝的,以至於正在接收的函數的外部沒有實體可以組合這個資料。這個拷貝對於避免共享資料的靜態條件是至關重要的。這個使得這個函數執行跟一個原子的操作是類似的。每一個函數呼叫都能跟任何其他的函數呼叫單獨的執行。
當一個函式呼叫可以被單獨執行的時候,每一個函式呼叫都可以在分離的CPU上執行。那就意味著,一個被實現的功能演算法就可以並行的執行,在多個CPU上。
使用Java 7,我們得到java.util.concurrent套件包含ForkAndJoinPool,這個可以幫助你實現跟功能並行性類似的事情。使用Java 8,我們得到一個並行streams,這個幫助你並行化大的集合的迭代。記住,有寫開發者對ForkAndJoinPool感到不滿的(在我的ForkAndJoinPool教程裡面你可以發現一些批評的連結)。
功能並行性最困難的部分就是去知道哪個函數呼叫去並行化。跨越CPU協作配合的函數呼叫帶來了一個開銷。被一個函數完成的工作單元需要某個大小的開銷。如果這個函數呼叫非常小,試著去並行化他們可能確實會比一個單執行緒執行,單獨的CPU執行會慢。
來自我的理解(這個當然不完美),你可以使用一個反應系統,時間驅動去實現一個演算法,以及去完成一個工作的分解,這個就是跟功能並行性類似的。伴隨著一個事件驅動模型,你只是得到一個確定更多的控制多少以及怎麼樣去並行化(我認為)。
另外,在多個CPU之上伴隨著協調配合的開銷分解一個任務,如果那個任務目前只是被這個程式執行的唯一任務才會有意義。然而,如果系統正在執行多個其他的任務(例如,web伺服器,資料庫伺服器以及許多其他的系統),那麼這裡就沒有意義去並行化一個單獨的任務。在電腦上的其他CPU不管怎麼都會忙著執行其他的任務,以至於沒有理由去使用一個更慢的功能並行的任務去打亂他們。你最可能明智的去使用一個管線並發模型,因為它有更小的開銷(在單執行緒模式下順序的執行),並且跟底層的硬體有更好的符合要求。
哪一個並發模型是最好的
那麼,哪一個並發模型是最好的呢?
通常就是這樣,這個答案取決於你的系統將會是什麼樣子的。如果你的任務是自然的並行的,獨立的,以及不需要有共享狀態的,那麼你可能會使用平行工作模型去實現你的系統。
許多任務儘管不是自然並行的和獨立的。對於這些種類的系統,我相信這個管線並發模型相對劣勢來說有更多的優勢,並且比平行工作者模型有更大的優勢。
你甚至不需要自己去寫管線結構的程式碼。現代的平台像Vert.x已經為你實現很多了。我個人將會在下一個專案探索運行在像Vert.x平台頂層之上的設計。我感覺,Java EE不會有任何優勢了。
以上就是Java 並發模型的詳細介紹的內容,更多相關內容請關注PHP中文網(m.sbmmt.com)!