Disruptor 全解析(3):写入 Ring Buffer原文地址:http://mechanitis.blogspot.com/2011/07/dissecting-disr
Disruptor 全解析(3):写入 Ring Buffer
原文地址:http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-writing-to-ring.html? 作者是 Trisha Gee, LMAX 公司的一位美女工程师:)
这是 Disruptor end-to-end view 中缺失的一节。当心了,它非常长。但是为了能让你在一起联系上下文阅读它,我还是决定把它们写进一篇博文中。
文章的重点是:不要让 Ring 重叠;如何通知消费者;生产者批处理;以及多个生产者如何协同工作。
ProducerBarriersDisruptor?代码? 为?
消费者?提供了一些接口和辅助类,但是没有为你的生产者提供接口。 生产者负责写入 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。待会儿我们会处理多生产者的复杂问题。
ConsumerTrackingProducerBarrier?对象拥有所有正在访问 Ring Buffer 的?
消费者?列表。这看起来有点儿奇怪-我从没有期望 ProducerBarrier 知道任何有关消费端那边的事情。但是等等,这是有原因的。因为我们不想和队列“混为一谈”(队列跟踪队列头和尾,它们有时候指向同一位置),消费者负责通知它们处理到了哪个序列号,而不是 Ring Buffer。所以,如果我们想确定我们没有让 buffer 重叠,我们需要检查消费者们都到了哪里。
在上图中,有一个?
消费者?顺利的跑到了最高序号(12,用红色/粉色高亮)。第二个?
消费者?有点儿落后-可能它在做 I/O 操作之类的-它在序号 3。因此消费者 2 在赶上消费者 1 之前要跑完整个 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 可以读到 13 和它前面的所有内容,之后它们都过得很 happy。
ProducerBarrier 批处理有趣的是 Disruptor 可以同时在生产端和?消费端? 两边实现批处理。还记得随着程序运行,消费者 2 最后到达了序号 9 吗?ProducerBarrier 可以在这里做一件很狡猾的事-它知道 buffer 的大小,也知道最慢的消费者位置。因此它能够发现现在哪些节点是可用的。
如果 ProducerBarrier 知道 Ring Buffer 的游标指向 12,而最慢的消费者在位置 9,它就可以让生产者写入节点 3,4,5,6,7 和 8,中间不需要再次检查消费者们的位置。
多个生产者这里你也许以为我讲完了,其实还有一些细节。
上面的图中我稍微撒了个谎。我暗示了 ProducerBarrier 拿到的序号直接来自 Ring Buffer 的游标。然而,如果你看代码的话,你会发现它是通过 ClaimStrategy 获取的。我省略这个是为了简化示意图,在单生产者的情况下它不是很重要。
在多生产者的场景下,你还需要其他东西来追踪序号。这是指当前可写入的序号。注意这和“Ring Buffer 游标加 1”不一样-如果你有超过一个以上的生产者在向 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:注意 2.0 版的 Disruptor 使用了和这篇文章里的不一样的命名。如果你对类名感到困惑,请阅读我的?变更总结?。
?
本文翻译自原作者博客, 译者是博客园?﹎〾敏ō?(http://www.cnblogs.com/adaikiss/??????), 我只是转载并整理了若干字句, 特别感谢。??