Java并发框架Disruptor教程(一)、概述

时间:2024-02-22 19:08:46

介绍

理解Disruptor是什么的最好方法是将它与目前很好理解和非常相似的东西进行比较。在Disruptor的情况下,这将是Java的BlockingQueue。与队列一样,Disruptor的目的是在同一进程内的线程之间移动数据(例如消息或事件)。但是,Disruptor提供了一些将其与队列区分开来的关键功能。他们是:

  • 具有消费者依赖关系图的消费者多播事件。
  • 为事件预先分配内存。
  • 无锁机制。

核心概念

在我们理解Disruptor如何工作之前,定义一些将在整个文档和代码中使用的术语是值得的。对于那些倾向于DDD的人来说,将其视为Disruptor域的普遍存在的语言。

  • Ring Buffer(环形缓冲区):
    环形缓冲区通常被认为是Disruptor的主要方面,但从3.0开始,环形缓冲区仅负责存储和更新通过Disruptor的数据(事件)。对于一些高级用例,可以完全由用户替换。
  • Sequence:
    Disruptor使用Sequences作为识别特定组件所在位置的方法。每个消费者(EventProcessor)都像Disruptor本身一样维护一个Sequence。大多数并发代码依赖于这些Sequence值的移动,因此Sequence支持AtomicLong的许多当前功能。事实上,两者之间唯一真正的区别是序列包含额外的功能,以防止序列和其他值之间的错误共享。
  • Sequencer:
    Sequencer是Disruptor的真正核心。该接口的两个实现(单生成器,多生产者)实现了所有并发算法,用于在生产者和消费者之间快速,正确地传递数据。
  • Sequence Barrier:
    序列屏障由序列发生器产生,包含对序列发生器中主要发布的序列和任何依赖性消费者的序列的引用。它包含确定是否有任何可供消费者处理的事件的逻辑。
  • Wait Strategy:
    等待策略确定消费者如何等待生产者将事件放入Disruptor。有关可选锁定的部分中提供了更多详细信息。
  • Event:
    从生产者传递给消费者的数据单位。事件没有特定的代码表示,因为它完全由用户定义。
  • EventProcessor:
    用于处理来自Disruptor的事件的主事件循环,并具有消费者序列的所有权。有一个名为 BatchEventProcessor的表示,它包含事件循环的有效实现,并将回调到使用的提供的EventHandler接口实现。
  • EventHandler:
    由用户实现并代表Disruptor的使用者的接口。
  • Producer:
    这是调用Disruptor以将事件排入队列的用户代码。这个概念在代码中也没有表示。

为了将这些元素置于上下文中,下面是LMAX如何在其高性能核心服务(例如交换)中使用Disruptor的示例。

图1.具有一组依赖消费者的干扰者。

在这里插入图片描述

多播事件

这是队列和Disruptor之间最大的行为差异。当您有多个消费者在同一个Disruptor上收听时,所有事件都会发布给所有消费者,而不是一个事件只发送给单个消费者的队列。Disruptor的行为旨在用于需要对同一数据进行独立多个并行操作的情况。来自LMAX的规范示例是我们有三个操作,即日志记录(将输入数据写入持久性日志文件),复制(将输入数据发送到另一台机器以确保存在数据的远程副本)和业务逻辑(真正的处理工作)。Executor风格的事件处理,通过在同一处并行处理不同的事件,也可以使用WorkerPool。请注意,它是在现有的Disruptor类之上进行的,并且不会使用相同的第一类支持进行处理,因此它可能不是实现该特定目标的最有效方法。

查看图1.可以看到有3个事件处理程序监听(JournalConsumer,ReplicationConsumer和ApplicationConsumer)到Disruptor,这些事件处理程序中的每一个都将接收Disruptor中可用的所有消息(按相同的顺序)。这允许每个消费者的工作并行运行。

消费者依赖图

为了支持并行处理行为的实际应用,有必要支持消费者之间的协调。返回参考上述示例,必须防止业务逻辑消费者在日记和复制消费者完成其任务之前取得进展。我们将此概念称为gating,或者更准确地说,这种行为的超集特征称为gating。gating发生在两个地方。首先,我们需要确保生产者不会超过消费者。这是通过调用RingBuffer.addGatingConsumers()将相关的使用者添加到Disruptor来处理的。其次,先前提到的情况是通过从必须首先完成其处理的组件构造包含序列的SequenceBarrier来实现的。

参考图1,有3个消费者正在收听来自Ring Buffer的事件。此示例中有一个依赖关系图。ApplicationConsumer依赖于JournalConsumer和ReplicationConsumer。这意味着JournalConsumer和ReplicationConsumer可以彼此并行*运行。从ApplicationConsumer的SequenceBarrier到JournalConsumer和ReplicationConsumer的序列的连接可以看到依赖关系。值得注意的是Sequencer与下游消费者之间的关系。它的一个作用是确保发布不覆盖Ring Buffer。要做到这一点,下游消费者中没有一个可能具有低于环形缓冲区序列的序列,而不是环形缓冲区的大小。但是,使用依赖关系图可以进行有趣的优化。由于ApplicationConsumers Sequence保证小于或等于JournalConsumer和ReplicationConsumer(这是该依赖关系所确保的),因此Sequencer只需要查看ApplicationConsumer的Sequence。在更一般的意义上,Sequencer只需要知道作为依赖关系树中叶节点的使用者的序列。

事件预分配

Disruptor的目标之一是在低延迟环境中使用。在低延迟系统中,必须减少或移除内存分配。在基于Java的系统中,目的是减少由于垃圾收集导致的数量停滞(在低延迟C / C ++系统中,由于存在于内存分配器上的争用,大量内存分配也存在问题)。

为了支持这一点,用户可以预先分配Disruptor中事件所需的存储空间。在构造期间,EventFactory由用户提供,并将在Disruptor的Ring Buffer中为每个条目调用。将新数据发布到Disruptor时,API将允许用户获取构造的对象,以便他们可以调用方法或更新该存储对象上的字段。Disruptor保证这些操作只要正确实现就是并发安全的。

无锁机制

低延迟期望推动的另一个关键实现细节是广泛使用无锁算法来实现Disruptor。所有内存可见性和正确性保证都是使用内存屏障和/或比较和交换操作实现的。只有一个用例需要实际锁定并且在BlockingWaitStrategy中。这仅仅是为了使用条件,以便在等待新事件到达时停放消耗线程。许多低延迟系统将使用忙等待来避免使用条件可能引起的抖动,但是在系统繁忙等待操作的数量中可能导致性能显着下降,尤其是在CPU资源严重受限的情况下。例如,虚拟化环境中的Web服务器。