閱讀926 返回首頁    go 汽車大全


如何使用 Disruptor(三)寫入 Ringbuffer

這是 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 中找到下一個節點,然後才允許生產者向它寫入數據。

Disruptor 全解析(3):寫入 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,但那樣的話這個例子就不夠有趣了)。

Disruptor 全解析(3):寫入 Ring Buffer
上圖顯示了當消費者 2 挪動到序號 9 時發生的情況。在這張圖中我已經忽略了ConsumerBarrier,因為它沒有參與這個場景。

ProducerBarier 會看到下一個節點——序號 3 那個已經可以用了。它會搶占這個節點上的 Entry(我還沒有特別介紹 Entry 對象,基本上它是一個放寫入到某個序號的 Ring Buffer 數據的桶),把下一個序號(13)更新成 Entry 的序號,然後把 Entry 返回給生產者。生產者可以接著往 Entry 裏寫入數據。

提交新的數據

兩階段提交的第二步是——對,提交。

Disruptor 全解析(3):寫入 Ring Buffer
綠色表示最近寫入的 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 的大小,也知道最慢的消費者位置。因此它能夠發現當前有哪些節點是可用的。

Disruptor 全解析(3):寫入 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), 直到 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)。

更新:最近的 RingBuffer​ 版本去掉了 Producer Barrier。如果在你看的代碼裏找不到 ProducerBarrier,那就假設當我講“Producer Barrier”時,我的意思是“Ring Buffer”。

更新2:注意 Disruptor 2.0 版使用了與本文不一樣的命名。如果你對類名感到困惑,請閱讀我寫的Disruptor 2.0更新摘要​。


文章轉自 並發編程網-ifeve.com

最後更新:2017-05-23 10:32:05

  上一篇:go  在寫作中成長
  下一篇:go  RecyclerView Part 1:為ListView專家寫的基礎