原文位址:
http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-writing-to-ring.html 作者是 Trisha Gee, LMAX 公司的一位女工程師。
這是 Disruptor 全方位解析(end-to-end view)中缺少的一章。當心,本文非常長。但是為了讓你能連在一起聯系上下文的閱讀,我還是決定把它們寫進一篇部落格裡。
本文的 重點 是:不要讓 Ring 重疊;如何通知消費者;生産者一端的批處理;以及多個生産者如何協同工作。
ProducerBarriers
Disruptor 代碼 給 消費者 提供了一些接口和輔助類,但是沒有給寫入 Ring Buffer 的 生産者 提供接口。這是因為除了你需要知道生産者之外,沒有别人需要通路它。盡管如此,Ring Buffer 還是與消費端一樣提供了一個 ProducerBarrier 對象,讓生産者通過它來寫入 Ring Buffer。
寫入 Ring Buffer 的過程涉及到兩階段送出 (two-phase commit)。首先,你的生産者需要申請 buffer 裡的下一個節點。然後,當生産者結束向節點寫入資料,它需要調用 ProducerBarrier 的 commit 方法。
那麼讓我們首先來看看第一步。它聽起來容易 -“給我 Ring Buffer 裡的下一個節點”。很好,從生産者角度來看它很簡單:簡單地調用 ProducerBarrier 的 nextEntry() 方法,這樣會傳回給你一個 Entry 對象,這個對象就是 Ring Buffer 的下一個節點。
ProducerBarrier 如何防止 Ring Buffer 重疊
在背景,由 ProducerBarrier 負責所有的互動細節來從 Ring Buffer 中找到下一個節點,然後才允許生産者向它寫入資料。

(我不确定
閃閃發亮的新手寫闆 能否有助于提高我畫的圖檔清晰度,但是它用起來很有意思)。
在這幅圖中,我們假設隻有一個生産者寫入 Ring Buffer。過一會兒我們再處理多個生産者的複雜問題。
ConsumerTrackingProducerBarrier 對象擁有所有正在通路 Ring Buffer 的 消費者 清單。這看起來有點兒奇怪-我從沒有期望 ProducerBarrier 了解任何有關消費端那邊的事情。但是等等,這是有原因的。因為我們不想與隊列“混為一談”(隊列需要追蹤隊列的頭和尾,它們有時候會指向相同的位置),Disruptor 由消費者負責通知它們處理到了哪個序列号,而不是 Ring Buffer。是以,如果我們想确定我們沒有讓 Ring Buffer 重疊,需要檢查所有的消費者們都讀到了哪裡。
在上圖中,有一個 消費者 順利的讀到了最大序号 12(用紅色/粉色高亮)。第二個消費者 有點兒落後——可能它在做 I/O 操作之類的——它停在序号 3。是以消費者 2 在趕上消費者 1 之前要跑完整個 Ring Buffer 一圈的距離。
現在生産者想要寫入 Ring Buffer 中序号 3 占據的節點,因為它是 Ring Buffer 目前遊标的下一個節點。但是 ProducerBarrier 明白現在不能寫入,因為有一個消費者正在占用它。是以,ProducerBarrier 停下來自旋 (spins),等待,直到那個消費者離開。
申請下一個節點
現在可以想像消費者 2 已經處理完了一批節點,并且向前移動了它的序号。可能它挪到了序号 9(因為消費端的批處理方式,現實中我會預期它到達 12,但那樣的話這個例子就不夠有趣了)。
上圖顯示了當消費者 2 挪動到序号 9 時發生的情況。在這張圖中我已經忽略了ConsumerBarrier,因為它沒有參與這個場景。
ProducerBarier 會看到下一個節點——序号 3 那個已經可以用了。它會搶占這個節點上的 Entry(我還沒有特别介紹 Entry 對象,基本上它是一個放寫入到某個序号的 Ring Buffer 資料的桶),把下一個序号(13)更新成 Entry 的序号,然後把 Entry 傳回給生産者。生産者可以接着往 Entry 裡寫入資料。
送出新的資料
兩階段送出的第二步是——對,送出。
綠色表示最近寫入的 Entry,序号是 13 ——厄,抱歉,我也是紅綠色盲。但是其他顔色甚至更糟糕。
當生産者結束向 Entry 寫入資料後,它會要求 ProducerBarrier 送出。
ProducerBarrier 先等待 Ring Buffer 的遊标追上目前的位置(對于單生産者這毫無意義-比如,我們已經知道遊标到了 12 ,而且沒有其他人正在寫入 Ring Buffer)。然後 ProducerBarrier 更新 Ring Buffer 的遊标到剛才寫入的 Entry 序号-在我們這兒是 13。接下來,ProducerBarrier 會讓消費者知道 buffer 中有新東西了。它戳一下 ConsumerBarrier 上的 WaitStrategy 對象說-“喂,醒醒!有事情發生了!”(注意-不同的 WaitStrategy 實作以不同的方式來實作提醒,取決于它是否采用阻塞模式。)
現在消費者 1 可以讀 Entry 13 的資料,消費者 2 可以讀 Entry 13 以及前面的所有資料,然後它們都過得很 happy。
ProducerBarrier 上的批處理
有趣的是 Disruptor 可以同時在生産者和
消費者 兩端實作批處理。還記得伴随着程式運作,消費者 2 最後達到了序号 9 嗎?ProducerBarrier 可以在這裡做一件很狡猾的事-它知道 Ring Buffer 的大小,也知道最慢的消費者位置。是以它能夠發現目前有哪些節點是可用的。
如果 ProducerBarrier 知道 Ring Buffer 的遊标指向 12,而最慢的消費者在 9 的位置,它就可以讓生産者寫入節點 3,4,5,6,7 和 8,中間不需要再次檢查消費者的位置。
多個生産者的場景
到這裡你也許會以為我講完了,但其實還有一些細節。
在上面的圖中我稍微撒了個謊。我暗示了 ProducerBarrier 拿到的序号直接來自 Ring Buffer 的遊标。然而,如果你看過代碼的話,你會發現它是通過 ClaimStrategy 擷取的。我省略這個對象是為了簡化示意圖,在單個生産者的情況下它不是很重要。
在多個生産者的場景下,你還需要其他東西來追蹤序号。這個序号是指目前可寫入的序号。注意這和“向 Ring Buffer 的遊标加 1”不一樣-如果你有一個以上的生産者同時在向 Ring Buffer 寫入,就有可能出現某些 Entry 正在被生産者寫入但還沒有送出的情況。
讓我們複習一下如何申請寫入節點。每個生産者都向 ClaimStrategy 申請下一個可用的節點。生産者 1 拿到序号 13,這和上面單個生産者的情況一樣。生産者 2 拿到序号 14,盡管 Ring Buffer的目前遊标僅僅指向 12。這是因為 ClaimSequence 不但負責分發序号,而且負責跟蹤哪些序号已經被配置設定。
現在每個生産者都擁有自己的寫入節點和一個嶄新的序号。
我把生産者 1 和它的寫入節點塗上綠色,把生産者 2 和它的寫入節點塗上可疑的粉色-看起來像紫色。
現在假設生産者 1 還生活在童話裡,因為某些原因沒有來得及送出資料。生産者 2 已經準備好送出了,并且向 ProducerBarrier 發出了請求。
就像我們先前在 commit 示意圖中看到的一樣,ProducerBarrier 隻有在 Ring Buffer 遊标到達準備送出的節點的前一個節點時它才會送出。在目前情況下,遊标必須先到達序号 13 我們才能送出節點 14 的資料。但是我們不能這樣做,因為生産者 1 正盯着一些閃閃發光的東西,還沒來得及送出。是以 ClaimStrategy 就停在那兒自旋 (spins), 直到 Bing Buffer 遊标到達它應該在的位置。
現在生産者 1 從迷糊中清醒過來并且申請送出節點 13 的資料(生産者 1 發出的綠色箭頭代表這個請求)。ProducerBarrier 讓 ClaimStrategy 先等待 Ring Buffer 的遊标到達序号 12,當然現在已經到了。是以 Ring Buffer 移動遊标到 13,讓 ProducerBarrier 戳一下 WaitStrategy 告訴所有人都知道 Ring Buffer 有更新了。現在 ProducerBarrier 可以完成生産者 2 的請求,讓 Bing Buffer 移動遊标到 14,并且通知所有人都知道。
你會看到,盡管生産者在不同的時間完成資料寫入,但是 Ring Buffer 的内容順序總是會遵循 nextEntry() 的初始調用順序。也就是說,如果一個生産者在寫入 Bing Buffer 的時候暫停了,隻有當它解除暫停後,其他等待中的送出才會立即執行。
呼——。我終于設法講完了這一切的内容并且一次也沒有提到記憶體屏障(Memory Barrier)。
更新:最近的
RingBuffer 版本去掉了 Producer Barrier。如果在你看的代碼裡找不到 ProducerBarrier,那就假設當我講“Producer Barrier”時,我的意思是“Ring Buffer”。
更新2:注意 Disruptor 2.0 版使用了與本文不一樣的命名。如果你對類名感到困惑,請閱讀我的
變更總結。