這是 disruptor 全方位解析(end-to-end view)中缺少的一章。當心,本文非常長。但是為了讓你能聯系上下文閱讀,我還是決定把它們寫進一篇部落格裡。
本文的 重點 是:不要讓 ring 重疊;如何通知消費者;生産者一端的批處理;以及多個生産者如何協同工作。
寫入 ring buffer 的過程涉及到兩階段送出 (two-phase commit)。首先,你的生産者需要申請 buffer 裡的下一個節點。然後,當生産者向節點寫完資料,它将會調用 producerbarrier 的 commit 方法。
那麼讓我們首先來看看第一步。 “給我 ring buffer
裡的下一個節點”,這句話聽起來很簡單。的确,從生産者角度來看它很簡單:簡單地調用 producerbarrier 的 nextentry()
方法,這樣會傳回給你一個 entry 對象,這個對象就是 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 知道 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), 直到 ring buffer 遊标到達它應該在的位置。
現在生産者 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)。