1. 程式人生 > >執行緒間共享資料無需競爭

執行緒間共享資料無需競爭

原文 地址  作者  Trisha   譯者:李同傑

LMAX Disruptor 是一個開源的併發框架,並獲得2011 Duke’s 程式框架創新獎。本文將用圖表的方式為大家介紹Disruptor是什麼,用來做什麼,以及簡單介紹背後的實現原理。

Disruptor是什麼?

Disruptor 是執行緒內通訊框架,用於執行緒裡共享資料。LMAX 建立Disruptor作為可靠訊息架構的一部分並將它設計成一種在不同元件中共享資料非常快的方法。
基於Mechanical Sympathy(對於計算機底層硬體的理解),基本的電腦科學以及領域驅動設計,Disruptor已經發展成為一個幫助開發人員解決很多繁瑣併發程式設計問題的框架。
很多架構都普遍使用一個佇列共享執行緒間的資料(即傳送訊息)。圖1 展示了一個在不同的階段中通過使用佇列來傳送訊息的例子(每個藍色的圈代表一個執行緒)。

1

圖 1

這種架構允許生產者執行緒(圖1中的stage1)在stage2很忙以至於無法立刻處理的時候能夠繼續執行下一步操作,從而提供瞭解決系統中資料擁堵的方法。這裡佇列可以看成是不同執行緒之間的緩衝。

在這種最簡單的情況下,Disruptor 可以用來代替佇列作為在不同的執行緒傳遞訊息的工具(如圖2所示)。

2

圖2

這種資料結構叫著RingBuffer,是用陣列實現的。Stage1執行緒把資料放進RingBuffer,而Stage2執行緒從RingBuffer中讀取資料。

圖2 中,可以看到RingBuffer中每格中都有序號,並且RingBuffer實時監測值最大(最新)的序號,該序號指向RingBuffer中最後一格。序號會伴隨著越來越多的資料增加進RingBuffer中而增長。

Disruptor的關鍵在於是它的設計目標是在框架內沒有競爭.這是通過遵守single-writer 原則,即只有一塊資料可以寫入一個數據塊中,而達到的。遵循這樣的規則使得Disruptor避免了代價高昂的CAS鎖,這也使得Disruptor非常快。

Disruptor通過使用RingBuffer以及每個事件處理器(EventProcessor)監測各自的序號從而減少了競爭。這樣,事件處理器只能更新自己所獲得的序號。當介紹向RingBuffer讀取和寫入資料時會對這個概念作進一步闡述。

釋出到Disruptor

向RingBuffer寫入資料需要通過兩階段提交(two-phase commit)。首先,Stage1執行緒即釋出者必須確定RingBuffer中下一個可以插入的格,如圖3所示。

3

圖 3

RingBuffer持有最近寫入格的序號(圖3中的18格),從而確定下一個插入格的序號。

RingBuffer通過檢查所有事件處理器正在從RingBuffer中讀取的當前序號來判斷下一個插入格是否空閒。

圖4顯示發現了下一個插入格。

4

圖 4

當釋出者得到下一個序號後,它可以獲得該格中的物件,並可以對該物件進行任意操作。你可以把格想象成一個簡單的可以寫入任意值的容器。

同時,在釋出者處理19格資料的時候,RingBuffer的序號依然是18,所以其他事件處理器將不會讀到19格中的資料。

圖5表示物件的改動儲存進了RingBuffer。

5

圖5

最終,釋出者最終將資料寫入19格後,通知RingBuffer釋出19格的資料。這時,RingBuffer更新序號並且所有從RingBuffer讀資料的事件處理器都可以看到19格中的資料。

RingBuffer中資料讀取

Disruptor框架中包含了可以從RingBuffer中讀取資料的BatchEventProcessor,下面將概述它如何工作並著重介紹它的設計。

當釋出者向RingBuffer請求下一個空格以便寫入時,一個實際上並不真的從RingBuffer消費事件的事件處理器,將監控它處理的最新的序號並請求它所需要的下一個序號。

圖5顯示事件處理器等待下一個序號。

6

圖6

事件處理器不是直接向RingBuffer請求序號,而是通過SequenceBarrier向RingBuffer請求序號。其中具體實現細節對我們的理解並不重要,但是下面可以看到這樣做的目的很明顯。

如圖6中Stage2所示,事件處理器的最大序號是16.它向SequenceBarrier呼叫waitFor(17)以獲得17格中的資料。因為沒有資料寫入RingBuffer,Stage2事件處理器掛起等待下一個序號。如果這樣,沒有什麼可以處理。但是,如圖6所示的情況,RingBuffer已經被填充到18格,所以waitFor函式將返回18並通知事件處理器,它可以讀取包括直到18格在內的資料,如圖7所示。

7

圖7

這種方法提供了非常好的批處理功能,可以在BatchEventProcessor原始碼中看到。原始碼中直接向RingBuffer批量獲取從下一個序號直到最大可以獲得的序號中的資料。

你可以通過實現EventHandler使用批處理功能。在Disruptor效能測試中有關於如何使用批處理的例子,例如FizzBuzzEventHandler。

是低延遲佇列?

當然,Disruptor可以被當作低延遲佇列來使用。我們對於Disruptor之前版本的測試資料顯示了,執行在一個2.2 GHz的英特爾酷睿i7-2720QM處理器上使用Java 1.6.0_25 64位的Ubuntu的11.04三層管道模式架構中,Disruptor比ArrayBlockingQueue快了多少。表1顯示了在管道中的每跳延遲。有關此測試的更多詳細資訊,請參閱Disruptor技術檔案。

但是不要根據延遲資料得出Disruptor只是一種解決某種特定效能問題的方案,因為它不是。

更酷的東西

一個有意思的事是Disruptor是如何支援系統元件之間的依賴關係,並在執行緒之間共享資料時不產生競爭。

Disruptor在設計上遵守single-writer 原則從而實現零競爭,即每個資料位只能被一個執行緒寫入。但是,這不代表你不可以使用多個執行緒讀資料,而這正是Disruptor所支援的。

Disruptor系統的最初設計是為了支援需要按照特定的順序發生的階段性類似流水線事件,這種需求在企業應用系統開發中並不少見。圖8顯示了標準的3級流水線。

8

圖 8

首先,每個事件都被寫入硬碟(日誌)作為日後恢復用。其次,這些事件被複制到備份伺服器。只有在這兩個階段後,系統開始業務邏輯處理。

按順序執行上次操作是一個合乎邏輯的方法,但是並不是最有效的方法。日誌和複製操作可以同步執行,因為他們互相獨立。但是業務邏輯必須在他們都執行完後才能執行。圖9顯示他們可以並行互不依賴。

9

圖 9

如果使用Disruptor,前兩個階段(日誌和複製)可以直接從RingBuffer中讀取資料。正如圖7種的簡化圖所示,他們都使用一個單一的Sequence Barrier從RingBuffer獲取下一個可用的序號。他們記錄他們使用過的序號,這樣他們知道那些事件已經讀過並可以使用BatchEventProcessor批量獲取事件。

業務邏輯同樣可以從同一個RingBuffer中讀取事件,但是隻限於前兩個階段已經處理過事件。這是通過加入第二個SequenceBarrier實現的,用它來監控處理日誌的事件處理器和複製的事件處理器,當請求最大可讀的序號時,它返回兩個處理器中較小的序號。

當每個事件處理器都使用SequenceBarrier 來確定哪些事件可以安全的從RingBuffer中讀出,那麼就從中讀出這些事件。

10

圖10

有很多事件處理器都可以從RingBuffer中讀取序號,包括日誌事件處理器,複製事件處理器等,但是隻有一個處理器可以增加序號。這保證了共享資料沒有競爭。

如果有多個釋出者?

Disruptor也支援多個釋出者向RingBuffer寫入。當然,因為這樣的話必然會發生兩個不同的事件處理器寫入同一格的情況,這樣就會產生競爭。Disruptor提供ClaimStrategy的處理方式應對有多個釋出者的情況。

結論

在這裡,我已經在總體上介紹了Disruptor框架是如何高效能線上程中共享資料,並簡單闡述了它的原理。有關更高階事件處理器以及向RingBuffer申請空間並等待下一個序號等很多策略在這裡都沒有涉及,Disruptor是開源的,到程式碼中去搜索吧。