線程間共享數據無需競爭
LMAX Disruptor 是一個開源的並發框架,並獲得2011 Duke’s 程序框架創新獎。本文將用圖表的方式為大家介紹Disruptor是什麼,用來做什麼,以及簡單介紹背後的實現原理。
Disruptor是什麼?
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讀取和寫入數據時會對這個概念作進一步闡述。
發布到Disruptor
向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格中的數據。
RingBuffer中數據讀取
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是開源的,到代碼中去搜索吧。
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-23 10:02:20