天天看點

線程間共享資料無需競争

Disruptor 是線程内通信架構,用于線程裡共享資料。LMAX 建立Disruptor作為可靠消息架構的一部分并将它設計成一種在不同元件中共享資料非常快的方法。

基于Mechanical Sympathy(對于計算機底層硬體的了解),基本的計算機科學以及領域驅動設計,Disruptor已經發展成為一個幫助開發人員解決很多繁瑣并發程式設計問題的架構。

很多架構都普遍使用一個隊列共享線程間的資料(即傳送消息)。圖1 展示了一個在不同的階段中通過使用隊列來傳送消息的例子(每個藍色的圈代表一個線程)。

線程間共享資料無需競争

圖 1

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

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

線程間共享資料無需競争

圖2

這種資料結構叫着RingBuffer,是用數組實作的。Stage1線程把資料放進RingBuffer,而Stage2線程從RingBuffer中讀取資料。

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

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

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

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

線程間共享資料無需競争

圖 3

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

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

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

線程間共享資料無需競争

圖 4

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

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

圖5表示對象的改動儲存進了RingBuffer。

線程間共享資料無需競争

圖5

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

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

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

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

線程間共享資料無需競争

圖6

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

如圖6中Stage2所示,事件處理器的最大序号是16.它向SequenceBarrier調用waitFor(17)以獲得17格中的資料。因為沒有資料寫入RingBuffer,Stage2事件處理器挂起等待下一個序号。如果這樣,沒有什麼可以處理。但是,如圖6所示的情況,RingBuffer已經被填充到18格,是以waitFor函數将傳回18并通知事件處理器,它可以讀取包括直到18格在内的資料,如圖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

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

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

線程間共享資料無需競争

圖 9

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

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

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

線程間共享資料無需競争

圖10

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

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

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