天天看點

如何使用 Disruptor(三)寫入 Ringbuffer

這是 disruptor 全方位解析(end-to-end view)中缺少的一章。當心,本文非常長。但是為了讓你能聯系上下文閱讀,我還是決定把它們寫進一篇部落格裡。

本文的 重點 是:不要讓 ring 重疊;如何通知消費者;生産者一端的批處理;以及多個生産者如何協同工作。

寫入 ring buffer 的過程涉及到兩階段送出 (two-phase commit)。首先,你的生産者需要申請 buffer 裡的下一個節點。然後,當生産者向節點寫完資料,它将會調用 producerbarrier 的 commit 方法。

那麼讓我們首先來看看第一步。 “給我 ring buffer

裡的下一個節點”,這句話聽起來很簡單。的确,從生産者角度來看它很簡單:簡單地調用 producerbarrier 的 nextentry()

方法,這樣會傳回給你一個 entry 對象,這個對象就是 ring buffer 的下一個節點。

在背景,由 producerbarrier 負責所有的互動細節來從 ring buffer 中找到下一個節點,然後才允許生産者向它寫入資料。

如何使用 Disruptor(三)寫入 Ringbuffer

在這幅圖中,我們假設隻有一個生産者寫入 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,但那樣的話這個例子就不夠有趣了)。

如何使用 Disruptor(三)寫入 Ringbuffer

上圖顯示了當消費者 2 挪動到序号 9 時發生的情況。在這張圖中我已經忽略了consumerbarrier,因為它沒有參與這個場景。

producerbarier 會看到下一個節點——序号 3 那個已經可以用了。它會搶占這個節點上的 entry(我還沒有特别介紹

entry 對象,基本上它是一個放寫入到某個序号的 ring buffer 資料的桶),把下一個序号(13)更新成 entry 的序号,然後把

entry 傳回給生産者。生産者可以接着往 entry 裡寫入資料。

兩階段送出的第二步是——對,送出。

如何使用 Disruptor(三)寫入 Ringbuffer

綠色表示最近寫入的 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。

如何使用 Disruptor(三)寫入 Ringbuffer

如果 producerbarrier 知道 ring buffer 的遊标指向 12,而最慢的消費者在 9 的位置,它就可以讓生産者寫入節點 3,4,5,6,7 和 8,中間不需要再次檢查消費者的位置。

到這裡你也許會以為我講完了,但其實還有一些細節。

在上面的圖中我稍微撒了個謊。我暗示了 producerbarrier 拿到的序号直接來自 ring buffer

的遊标。然而,如果你看過代碼的話,你會發現它是通過 claimstrategy

擷取的。我省略這個對象是為了簡化示意圖,在單個生産者的情況下它不是很重要。

在多個生産者的場景下,你還需要其他東西來追蹤序号。這個序号是指目前可寫入的序号。注意這和“向 ring buffer 的遊标加

1”不一樣-如果你有一個以上的生産者同時在向 ring buffer 寫入,就有可能出現某些 entry 正在被生産者寫入但還沒有送出的情況。

如何使用 Disruptor(三)寫入 Ringbuffer

讓我們複習一下如何申請寫入節點。每個生産者都向 claimstrategy 申請下一個可用的節點。生産者 1 拿到序号

13,這和上面單個生産者的情況一樣。生産者 2 拿到序号 14,盡管 ring buffer的目前遊标僅僅指向 12。這是因為

claimsequence 不但負責分發序号,而且負責跟蹤哪些序号已經被配置設定。

現在每個生産者都擁有自己的寫入節點和一個嶄新的序号。

我把生産者 1 和它的寫入節點塗上綠色,把生産者 2 和它的寫入節點塗上可疑的粉色-看起來像紫色。

如何使用 Disruptor(三)寫入 Ringbuffer

現在假設生産者 1 還生活在童話裡,因為某些原因沒有來得及送出資料。生産者 2 已經準備好送出了,并且向 producerbarrier 發出了請求。

就像我們先前在 commit 示意圖中看到的一樣,producerbarrier 隻有在 ring buffer

遊标到達準備送出的節點的前一個節點時它才會送出。在目前情況下,遊标必須先到達序号 13 我們才能送出節點 14

的資料。但是我們不能這樣做,因為生産者 1 正盯着一些閃閃發光的東西,還沒來得及送出。是以 claimstrategy 就停在那兒自旋

(spins), 直到 ring buffer 遊标到達它應該在的位置。

如何使用 Disruptor(三)寫入 Ringbuffer

現在生産者 1 從迷糊中清醒過來并且申請送出節點 13 的資料(生産者 1 發出的綠色箭頭代表這個請求)。producerbarrier 讓

claimstrategy 先等待 ring buffer 的遊标到達序号 12,當然現在已經到了。是以 ring buffer 移動遊标到

13,讓 producerbarrier 戳一下 waitstrategy 告訴所有人都知道 ring buffer 有更新了。現在

producerbarrier 可以完成生産者 2 的請求,讓 ring buffer 移動遊标到 14,并且通知所有人都知道。

你會看到,盡管生産者在不同的時間完成資料寫入,但是 ring buffer 的内容順序總是會遵循 nextentry()

的初始調用順序。也就是說,如果一個生産者在寫入 ring buffer 的時候暫停了,隻有當它解除暫停後,其他等待中的送出才會立即執行。

呼——。我終于設法講完了這一切的内容并且一次也沒有提到記憶體屏障(memory barrier)。