如何使用 Disruptor(三)写入 Ringbuffer

来源:互联网 发布:徽标在线制作软件 编辑:程序博客网 时间:2024/06/05 17:10

作者Trisha  译者:廖涵 校对:方腾飞

这是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的位置,它就可以让生产者写入节点 34567 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本文链接地址: 如何使用 Disruptor(三)写入Ringbuffer

并发框架Disruptor译文

如何使用Disruptor(一)Ringbuffer的特别之处

如何使用Disruptor(二)如何从Ringbuffer读取

如何使用Disruptor(三)写入 Ringbuffer

解析Disruptor的依赖关系

Disruptor(无锁并发框架)-发布

LMAX Disruptor——一个高性能、低延迟且简单的框架

Disruptor Wizard已死,Disruptor Wizard永存!

Disruptor 2.0更新摘要

线程间共享数据无需竞争