1. 程式人生 > >JAVA併發程式設計--併發模式

JAVA併發程式設計--併發模式

並行工作者

並行工作者模型的優點

並行工作者模式的優點是,它很容易理解。你只需新增更多的工作者來提高系統的並行度。

例如,如果你正在做一個網路爬蟲,可以試試使用不同數量的工作者抓取到一定數量的頁面,然後看看多少數量的工作者消耗的時間最短(意味著效能最高)。由於網路爬蟲是一個IO密集型工作,最終結果很有可能是你電腦中的每個CPU或核心分配了幾個執行緒。每個CPU若只分配一個執行緒可能有點少,因為在等待資料下載的過程中CPU將會空閒大量時間。

並行工作者模型的缺點

並行工作者模型雖然看起來簡單,卻隱藏著一些缺點。接下來的章節中我會分析一些最明顯的弱點。

1.共享狀態可能會很複雜

在實際應用中,並行工作者模型可能比前面所描述的情況要複雜得多。共享的工作者經常需要訪問一些共享資料,無論是記憶體中的或者共享的資料庫中的。下圖展示了並行工作者模型是如何變得複雜的:


有些共享狀態是在像作業佇列這樣的通訊機制下。但也有一些共享狀態是業務資料,資料快取,資料庫連線池等。

一旦共享狀態潛入到並行工作者模型中,將會使情況變得複雜起來。執行緒需要以某種方式存取共享資料,以確保某個執行緒的修改能夠對其他執行緒可見(資料修改需要同步到主存中,不僅僅將資料儲存在執行這個執行緒的CPU的快取中)。執行緒需要避免竟態,死鎖以及很多其他共享狀態的併發性問題。

此外,在等待訪問共享資料結構時,執行緒之間的互相等待將會丟失部分並行性。許多併發資料結構是阻塞的,意味著在任何一個時間只有一個或者很少的執行緒能夠訪問。這樣會導致在這些共享資料結構上出現競爭狀態。在執行需要訪問共享資料結構部分的程式碼時,高競爭基本上會導致執行時出現一定程度的序列化。

解決辦法:

非阻塞併發演算法

可持久化資料結構: http://www.cnblogs.com/tedzhao/archive/2008/11/12/1332112.html

在修改的時候,可持久化的資料結構總是保護它的前一個版本不受影響。

2.無狀態工作者

共享狀態能夠被系統中得其他執行緒修改。所以工作者在每次需要的時候必須重讀狀態,以確保每次都能訪問到最新的副本,不管共享狀態是儲存在記憶體中的還是在外部資料庫中。工作者無法在內部儲存這個狀態(但是每次需要的時候可以重讀)稱為無狀態的。

3. 任務順序是不確定的

並行工作者模式的這種非確定性的特性,使得很難在任何特定的時間點推斷系統的狀態。這也使得它也更難(如果不是不可能的話)保證一個作業在其他作業之前被執行。

流水線模式


也成無共享並行模式

一般使用非阻塞的io設計流水線模式。

不會一直等待IO操作的結束。IO操作速度很慢,所以等待IO操作結束很浪費CPU時間。此時CPU可以做一些其他事情。當IO操作完成的時候,IO操作的結果(比如讀出的資料或者資料寫完的狀態)被傳遞給下一個工作者。

有了非阻塞IO,就可以使用IO操作確定工作者之間的邊界。工作者會盡可能多執行直到遇到並啟動一個IO操作。然後交出作業的控制權。當IO操作完成的時候,在流水線上的下一個工作者繼續進行操作,直到它也遇到並啟動一個IO操作。


而實際上是:

                            

反應器,事件驅動系統

採用流水線併發模型的系統有時候也稱為反應器系統或事件驅動系統。系統內的工作者對系統內出現的事件做出反應,這些事件也有可能來自於外部世界或者發自其他工作者。事件可以是傳入的HTTP請求,也可以是某個檔案成功載入到記憶體中等。在寫這篇文章的時候,已經有很多有趣的反應器/事件驅動平臺可以使用了,並且不久的將來會有更多。比較流行的似乎是這幾個:

·       AKKa

·       Node.JS(JavaScript)

一般應用模型

Actor model和Channels

Actor model

在Actor模型中每個工作者被稱為actor。Actor之間可以直接非同步地傳送和處理訊息。Actor可以被用來實現一個或多個像前文描述的那樣的作業處理流水線。下圖給出了Actor模型


Channel model

而在Channel模型中,工作者之間不直接進行通訊。相反,它們在不同的通道中釋出自己的訊息(事件)。其他工作者們可以在這些通道上監聽訊息,傳送者無需知道誰在監聽。下圖給出了Channel模型:


流水線模型的優點

相比並行工作者模型,流水線併發模型具有幾個優點,在接下來的章節中我會介紹幾個最大的優點。

無需共享的狀態

工作者之間無需共享狀態,意味著實現的時候無需考慮所有因併發訪問共享物件而產生的併發性問題。這使得在實現工作者的時候變得非常容易。在實現工作者的時候就好像是單個執行緒在處理工作-基本上是一個單執行緒的實現。

有狀態的工作者

當工作者知道了沒有其他執行緒可以修改它們的資料,工作者可以變成有狀態的。對於有狀態,我是指,它們可以在記憶體中儲存它們需要操作的資料,只需在最後將更改寫回到外部儲存系統。因此,有狀態的工作者通常比無狀態的工作者具有更高的效能。

較好的硬體整合(HardwareConformity)

單執行緒程式碼在整合底層硬體的時候往往具有更好的優勢。首先,當能確定程式碼只在單執行緒模式下執行的時候,通常能夠建立更優化的資料結構和演算法。

其次,像前文描述的那樣,單執行緒有狀態的工作者能夠在記憶體中快取資料。在記憶體中快取資料的同時,也意味著資料很有可能也快取在執行這個執行緒的CPU的快取中。這使得訪問快取的資料變得更快。

我說的硬體整合是指,以某種方式編寫的程式碼,使得能夠自然地受益於底層硬體的工作原理。有些開發者稱之為mechanical sympathy。我更傾向於硬體整合這個術語,因為計算機只有很少的機械部件,並且能夠隱喻“更好的匹配(match better)”,相比“同情(sympathy)”這個詞在上下文中的意思,我覺得“conform”這個詞表達的非常好。當然了,這裡有點吹毛求疵了,用自己喜歡的術語就行。

合理的作業順序

基於流水線併發模型實現的併發系統,在某種程度上是有可能保證作業的順序的。作業的有序性使得它更容易地推出系統在某個特定時間點的狀態。更進一步,你可以將所有到達的作業寫入到日誌中去。一旦這個系統的某一部分掛掉了,該日誌就可以用來重頭開始重建系統當時的狀態。按照特定的順序將作業寫入日誌,並按這個順序作為有保障的作業順序。下圖展示了一種可能的設計:


實現一個有保障的作業順序是不容易的,但往往是可行的。如果可以,它將大大簡化一些任務,例如備份、資料恢復、資料複製等,這些都可以通過日誌檔案來完成。

流水線模型的缺點

流水線併發模型最大的缺點是作業的執行往往分佈到多個工作者上,並因此分佈到專案中的多個類上。這樣導致在追蹤某個作業到底被什麼程式碼執行時變得困難。

同樣,這也加大了程式碼編寫的難度。有時會將工作者的程式碼寫成回撥處理的形式。若在程式碼中嵌入過多的回撥處理,往往會出現所謂的回撥地獄(callback hell)現象。所謂回撥地獄,就是意味著在追蹤程式碼在回撥過程中到底做了什麼,以及確保每個回撥只訪問它需要的資料的時候,變得非常困難

使用並行工作者模型可以簡化這個問題。你可以開啟工作者的程式碼,從頭到尾優美的閱讀被執行的程式碼。當然並行工作者模式的程式碼也可能同樣分佈在不同的類中,但往往也能夠很容易的從程式碼中分析執行的順序。

函式式並行(Functional Parallelism)


第三種併發模型是函式式並行模型,這是也最近(2015)討論的比較多的一種模型。函式式並行的基本思想是採用函式呼叫實現程式。函式可以看作是”代理人(agents)“或者”actor“,函式之間可以像流水線模型(AKA 反應器或者事件驅動系統)那樣互相傳送訊息。某個函式呼叫另一個函式,這個過程類似於訊息傳送。

函式都是通過拷貝來傳遞引數的,所以除了接收函式外沒有實體可以操作資料。這對於避免共享資料的競態來說是很有必要的。同樣也使得函式的執行類似於原子操作。每個函式呼叫的執行獨立於任何其他函式的呼叫。

一旦每個函式呼叫都可以獨立的執行,它們就可以分散在不同的CPU上執行了。這也就意味著能夠在多處理器上並行的執行使用函式式實現的演算法。

Java7中的java.util.concurrent包裡包含的ForkAndJoinPool能夠幫助我們實現類似於函式式並行的一些東西。而Java8中並行streams能夠用來幫助我們並行的迭代大型集合。記住有些開發者對ForkAndJoinPool進行了批判(你可以在我的ForkAndJoinPool教程裡面看到批評的連結)。

函式式並行裡面最難的是,確定需要並行的那個函式呼叫。跨CPU協調函式呼叫需要一定的開銷。某個函式完成的工作單元需要達到某個大小以彌補這個開銷。如果函式呼叫作用非常小,將它並行化可能比單執行緒、單CPU執行還慢。

我個人認為(可能不太正確),你可以使用反應器或者事件驅動模型實現一個演算法,像函式式並行那樣的方法實現工作的分解。使用事件驅動模型可以更精確的控制如何實現並行化(我的觀點)。

此外,將任務拆分給多個CPU時協調造成的開銷,僅僅在該任務是程式當前執行的唯一任務時才有意義。但是,如果當前系統正在執行多個其他的任務時(比如web伺服器,資料庫伺服器或者很多其他類似的系統),將單個任務進行並行化是沒有意義的。不管怎樣計算機中的其他CPU們都在忙於處理其他任務,沒有理由用一個慢的、函式式並行的任務去擾亂它們。使用流水線(反應器)併發模型可能會更好一點,因為它開銷更小(在單執行緒模式下順序執行)同時能更好的與底層硬體整合。