Disruptor(無鎖併發框架)-釋出
譯者:羅立樹
假如你生活在另外一個星球,我們最近開源了一套高效能的基於訊息傳遞的開源框架。
下面我給大家介紹一下如何將訊息通過Ring buffer在無鎖的情況下進行處理。
在深入介紹之前,可以先快速閱讀一下Trish發表的文章,該文章介紹了ring buffer和其工作原理。
這篇文章的要點如下:
1.ring buffer是由一個大陣列組成的。
2.所有ring buffer的“指標”(也稱為序列或遊標)是java long型別的(64位有符號數),指標採用往上計數自增的方式。(不用擔心越界,即使每秒1,000,000條訊息,也要消耗300,000年才可以用完)。
3.對ring buffer中的指標進行按ring buffer的size取模找出陣列的下標來定位入口(類似於HashMap的entry)。為了提高效能,我們通常將ring buffer的size大小設定成實際使用的2倍。
這樣我們可以通過位運算(bit-mask )的方式計算出陣列的下標。
Ring buffer的基礎結構
注意:和程式碼中的實際實現,我這裡描述的內容是進行了簡化和抽象的。從概念上講,我認為更加方面理解。
ring buffer維護兩個指標,“next”和“cursor”。
在上面的圖示裡,是一個size為7的ring buffer(你應該知道這個手工繪製的圖示的原理),從0-2的座標位置是填充了資料的。
“next”指標指向第一個未填充資料的區塊。“cursor”指標指向最後一個填充了資料的區塊。在一個空閒的ring bufer中,它們是彼此緊鄰的,如上圖所示。
填充資料(Claiming a slot,獲取區塊)
Disruptor API 提供了事務操作的支援。當從ring buffer獲取到區塊,先是往區塊中寫入資料,然後再進行提交的操作。
假設有一個執行緒負責將字母“D”寫進ring buffer中。將會從ring buffer中獲取一個區塊(slot),這個操作是一個基於CAS的“get-and-increment”操作,將“next”指標進行自增。這樣,當前執行緒(我們可以叫做執行緒D)進行了get-and-increment操作後,
指向了位置4,然後返回3。這樣,執行緒D就獲得了位置3的操作許可權。
接著,另一個執行緒E做類似以上的操作。
提交寫入
以上,執行緒D和執行緒E都可以同時執行緒安全的往各自負責的區塊(或位置,slots)寫入資料。但是,我們可以討論一下執行緒E先完成任務的場景…
執行緒E嘗試提交寫入資料。在一個繁忙的迴圈中有若干的CAS提交操作。執行緒E持有位置4,它將會做一個CAS的waiting操作,直到 “cursor”變成3,然後將“cursor”變成4。
再次強調,這是一個原子性的操作。因此,現在的ring buffer中,“cursor”現在是2,執行緒E將會進入長期等待並重試操作,直到 “cursor”變成3。
然後,執行緒D開始提交。執行緒E用CAS操作將“cursor”設定為3(執行緒E持有的區塊位置)當且僅當“cursor”位置是2.“cursor”當前是2,所以CAS操作成功和提交也成功了。
這時候,“cursor”已經更新成3,然後所有和3相關的資料變成可讀。
這是一個關鍵點。知道ring buffer填充了多少 – 即寫了多少資料,那一個序列數寫入最高等等,是遊標的一些簡單的功能。“next”指標是為了保證寫入的事務特性。
最後的疑惑是執行緒E的寫入可見,執行緒E一直重試,嘗試將“cursor”從3更新成4,經過執行緒D操作後已經更新成3,那麼下一次重試就可以成功了。
總結
寫入資料可見的先後順序是由執行緒所搶佔的位置的先後順序決定的,而不是由它的提交先後決定的。但你可以想象這些執行緒從網路層中獲取訊息,這是和訊息按照時間到達的先後順序是沒什麼不同的,而兩個執行緒競爭獲取一個不同循序的位置。
因此,這是一個簡單而優雅的演算法,寫操作是原子的,事務性和無鎖,即使有多個寫入執行緒。