我想了解disruptor pattern。我看过InfoQ视频,试图阅读他们的论文。我知道有一个环形缓冲区,它被初始化为一个非常大的数组,以利用缓存局部性,消除新内存的分配。
听起来有一个或多个原子整数可以跟踪位置。每个'事件'似乎都得到一个唯一的id,它在环中的位置是通过找到与模数相关的模数等来找到的。
不幸的是,我对它的工作方式没有直观的认识。我做了很多交易应用程序并研究了actor model,查看了SEDA等等。
在他们的演讲中,他们提到这种模式基本上是路由器的工作方式;但是我没有找到路由器如何工作的任何好的描述。
是否有一些更好解释的好指示?
答案 0 :(得分:207)
Google Code项目对环形缓冲区的实施做了reference a technical paper,但对于想要了解其工作原理的人来说,它有点干,学术性和难度。然而,有一些博客文章已经开始以更易读的方式解释内部。有一个explanation of ring buffer是破坏者模式的核心,description of the consumer barriers(与破坏者阅读相关的部分)和一些information on handling multiple producers可用。
最简单的Disruptor描述是:它是一种以尽可能最有效的方式在线程之间发送消息的方法。它可以用作队列的替代品,但它也与SEDA和Actors共享许多功能。
与队列相比:
Disruptor提供了将消息传递到另一个线程的能力,如果需要可以将其唤醒(类似于BlockingQueue)。但是,有3个明显的差异。
与演员比较
Actor模型比大多数其他编程模型更接近Disruptor,特别是如果您使用提供的BatchConsumer / BatchHandler类。这些类隐藏了维护消耗的序列号的所有复杂性,并在发生重要事件时提供一组简单的回调。但是,有一些细微的差别。
onEndOfBatch()
。这允许缓慢的消费者,例如那些进行I / O批处理事件以提高吞吐量的人。可以在其他Actor框架中进行批处理,但由于几乎所有其他框架都不在批处理结束时提供回调,因此需要使用超时来确定批处理的结束,从而导致延迟较差。与SEDA相比
LMAX构建了Disruptor模式来取代基于SEDA的方法。
与内存障碍相比
另一种思考方式是作为一种结构化的有序内存屏障。生产者屏障形成写屏障,消费者屏障是阅读障碍。
答案 1 :(得分:134)
首先,我们想了解它提供的编程模型。
有一个或多个作家。有一个或多个读者。有一系列条目,完全从旧到新排序(如左图所示)。作家可以在右端添加新条目。每个读者从左到右依次读取条目。显然,读者无法阅读过去的作家。
没有条目删除的概念。我使用“阅读器”而不是“消费者”来避免使用条目的图像。但是我们知道最后一个读者左边的条目变得毫无用处。
一般来说,读者可以同时独立阅读。但是我们可以在读者之间声明依赖关系。读者依赖可以是任意非循环图。如果读者B依赖于读者A,则读者B无法读取读者A。
读者依赖性的产生是因为读者A可以注释一个条目,而读者B依赖于该注释。例如,A对条目进行一些计算,并将结果存储在条目中的字段a
中。然后继续前进,现在B可以读取条目,并存储a
A的值。如果读者C不依赖于A,则C不应尝试阅读a
。
这确实是一个有趣的编程模型。无论性能如何,单独的模型都可以使许多应用受益。
当然,LMAX的主要目标是性能。它使用预先分配的条目环。环足够大,但是它的界限使得系统不会超出设计容量。如果戒指已满,作者将等到最慢的读者前进并腾出空间。
Entry对象已预先分配并永久存在,以减少垃圾收集成本。我们不插入新的条目对象或删除旧的条目对象,而是写入者要求预先存在的条目,填充其字段并通知读者。这种明显的两阶段行动实际上只是一个原子行动
setNewEntry(EntryPopulator);
interface EntryPopulator{ void populate(Entry existingEntry); }
预分配条目还意味着相邻条目(很可能)位于相邻的存储单元中,并且由于读取器按顺序读取条目,因此利用CPU缓存非常重要。
还有很多努力来避免锁定,CAS,甚至是内存障碍(例如,如果只有一个编写器,则使用非易失性序列变量)
对于读者的开发者:不同的注释读者应该写入不同的字段,以避免写入争用。 (实际上他们应该写入不同的缓存行。)注释读者不应该触及其他非依赖读者可能阅读的内容。这就是为什么我说这些读者注释条目,而不是修改条目。
答案 2 :(得分:41)
Martin Fowler撰写了一篇关于LMAX和破坏者模式The LMAX Architecture的文章,该文章可能会进一步澄清。
答案 3 :(得分:17)
我实际上是花时间研究实际的来源,出于纯粹的好奇心,其背后的想法非常简单。撰写本文时的最新版本是3.2.1。
有一个缓冲区存储预先分配的事件,这些事件将保存消费者阅读的数据。
缓冲区由其长度的标志数组(整数数组)支持,该数组描述缓冲区插槽的可用性(有关详细信息,请参阅更多信息)。该数组的访问方式类似于java#AtomicIntegerArray,因此为了进行此扩展,您可以将其视为一个。
可以有任意数量的生产者。当生产者想要写入缓冲区时,会生成一个长数字(如在调用AtomicLong#getAndIncrement时,Disruptor实际上使用它自己的实现,但它以相同的方式工作)。让我们调用这个生成的long一个producerCallId。以类似的方式,当消费者ENDS从缓冲区读取时隙时,生成consumerCallId。访问最新的consumerCallId。
(如果有很多消费者,则选择ID最低的呼叫。)
然后比较这些ID,如果两者之间的差异小于缓冲区,则允许生产者写入。
(如果producerCallId大于最近的consumerCallId + bufferSize,则意味着缓冲区已满,并且生产者被迫总线等待,直到有一个点可用。)
然后根据他的callId(它是prducerCallId modulo bufferSize)为生产者分配缓冲区中的槽,但由于bufferSize总是2的幂(在缓冲区创建时强制执行限制),因此使用的执行操作是producerCallId& (bufferSize - 1))。然后可以自由修改该插槽中的事件。(实际的算法有点复杂,涉及将最近的consumerId缓存在单独的原子引用中,以进行优化。)
修改事件后,更改已发布"。当发布标志数组中的相应插槽时,将填充更新的标志。标志值是循环的数量(producerCallId除以bufferSize(同样,因为bufferSize是2的幂,实际操作是右移)。
以类似的方式,可以有任意数量的消费者。每次消费者想要访问缓冲区时,都会生成consumerCallId(取决于消费者如何被添加到破坏者中,id生成中使用的原子可以为每个用户共享或分离)。然后将此consumerCallId与最新的producentCallId进行比较,如果它是两者中的较小者,则允许读者继续进行。
(类似地,如果producerCallId甚至是consumerCallId,则意味着缓冲区是安全的并且消费者被迫等待。等待的方式由在创建破坏者期间的WaitStrategy定义。)
对于个人消费者(具有他们自己的id生成器的消费者),接下来检查的是批量消费的能力。缓冲区中的槽按顺序从对应的customerCallId(索引以与生产者相同的方式确定)的顺序检查,分别对应于最近的producerCallId。
通过比较标志数组中写入的标志值与为consumerCallId生成的标志值,在循环中检查它们。如果标志匹配则意味着填充插槽的生产者已经提交了他们的更改。如果不是,则循环中断,并返回最高的提交changeId。从ConsumerCallId到changeId中接收的插槽可以批量使用。
如果一组消费者一起阅读(具有共享id生成器的消费者),则每个消费者只需要一个callId,并且只检查并返回该单个callId的插槽。
答案 4 :(得分:7)
来自this article:
disruptor模式是一个由循环备份的批处理队列 阵列(即环形缓冲区)充满预先分配的传输 使用内存屏障同步生成器和对象的对象 消费者通过序列。
记忆障碍很难解释,Trisha的博客在这篇文章中做了最好的尝试:http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast.html
但是如果你不想深入了解低级细节,你可以知道Java中的内存障碍是通过volatile
关键字或java.util.concurrent.AtomicLong
实现的。破坏者模式序列是AtomicLong
,并且通过内存障碍而不是锁来在生产者和消费者之间来回传递。
我发现通过代码更容易理解一个概念,所以下面的代码是来自CoralQueue的一个简单的 helloworld ,这是一个由我所属的CoralBlocks完成的破坏者模式实现。在下面的代码中,您可以看到破坏程序模式如何实现批处理以及环形缓冲区(即循环数组)如何允许两个线程之间的无垃圾通信:
package com.coralblocks.coralqueue.sample.queue;
import com.coralblocks.coralqueue.AtomicQueue;
import com.coralblocks.coralqueue.Queue;
import com.coralblocks.coralqueue.util.MutableLong;
public class Sample {
public static void main(String[] args) throws InterruptedException {
final Queue<MutableLong> queue = new AtomicQueue<MutableLong>(1024, MutableLong.class);
Thread consumer = new Thread() {
@Override
public void run() {
boolean running = true;
while(running) {
long avail;
while((avail = queue.availableToPoll()) == 0); // busy spin
for(int i = 0; i < avail; i++) {
MutableLong ml = queue.poll();
if (ml.get() == -1) {
running = false;
} else {
System.out.println(ml.get());
}
}
queue.donePolling();
}
}
};
consumer.start();
MutableLong ml;
for(int i = 0; i < 10; i++) {
while((ml = queue.nextToDispatch()) == null); // busy spin
ml.set(System.nanoTime());
queue.flush();
}
// send a message to stop consumer...
while((ml = queue.nextToDispatch()) == null); // busy spin
ml.set(-1);
queue.flush();
consumer.join(); // wait for the consumer thread to die...
}
}