Discuptor入门(二)-实例

前言:最近在项目中看到有人使用的discuptor框架,因为没有接触过所以网上找了些资料.但最终发现开荒者太少,好像没什么人用那.最后感觉还是官方入门文档靠谱点.所以自己翻译了下(翻译器~),希望能帮助到别人.后续如果有什么新理解,我会继续补充的.

discuptor简介:高并发无锁框架

原文地址:https://github.com/LMAX-Exchange/disruptor/wiki/Getting-Started
此为译文
愿所有码农能看的英文~

ps:阅读前你需要花几分钟时间去了解一下discuptor的基本概念

 

为了开始使用Disruptor,我们将考虑一个非常简单和人为的例子,一个将生产者传递给消费者的单个长值,消费者只需打印出该值。 首先,我们将定义将携带数据的事件。

public class LongEvent
{
    private long value;

    public void set(long value)
    {
        this.value = value;
    }
}

 

为了让Disruptor为我们预先分配这些事件,我们需要一个将执行构造的EventFactory

public class LongEventFactory implements EventFactory<LongEvent>
{
    public LongEvent newInstance()
    {
        return new LongEvent();
    }
}

 

一旦我们定义了事件,我们需要创建一个处理这些事件的消费者。 在我们的例子中,我们要做的就是从控制台中打印出值

public class LongEventHandler implements EventHandler<LongEvent>
{
    public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
    {
        System.out.println("Event: " + event);
    }
}

 

我们将需要这些事件的来源,为了举例,我将假设数据来自某种I / O设备,例如, 网络或文件以ByteBuffer的形式

使用Disruptor的3.0版本,添加了更丰富的Lambda风格的API,以帮助开发人员将此复杂性封装在Ring Buffer中,因此3.0之后发布消息的首选方法是通过API的Event Publisher / Event Translator部分。
例如:

public class LongEventProducerWithTranslator
{
    private final RingBuffer<LongEvent> ringBuffer;
    
    public LongEventProducerWithTranslator(RingBuffer<LongEvent> ringBuffer)
    {
        this.ringBuffer = ringBuffer;
    }
    
    private static final EventTranslatorOneArg<LongEvent, ByteBuffer> TRANSLATOR =
        new EventTranslatorOneArg<LongEvent, ByteBuffer>()
        {
            public void translateTo(LongEvent event, long sequence, ByteBuffer bb)
            {
                event.set(bb.getLong(0));
            }
        };

    public void onData(ByteBuffer bb)
    {
        ringBuffer.publishEvent(TRANSLATOR, bb);
    }
}

 

这种方法的另一个优点是翻译器代码可以被拉入一个单独的类中,并可以轻松地单独进行单元测试。 Disruptor提供了许多不同的接口(EventTranslator,EventTranslatorOneArg,EventTranslatorTwoArg等),可以实现这些接口以提供翻译。 原因是允许转换器表示为静态类或非捕获lambda(当Java 8滚动时)作为转换方法的参数通过Ring Buffer上的调用传递给转换器。

使用旧版API发布
我们可以使用更“原始”的方法。

public class LongEventProducer
{
    private final RingBuffer<LongEvent> ringBuffer;

    public LongEventProducer(RingBuffer<LongEvent> ringBuffer)
    {
        this.ringBuffer = ringBuffer;
    }

    public void onData(ByteBuffer bb)
    {
        long sequence = ringBuffer.next();  // Grab the next sequence
        try
        {
            LongEvent event = ringBuffer.get(sequence); // Get the entry in the Disruptor
                                                        // for the sequence
            event.set(bb.getLong(0));  // Fill with data
        }
        finally
        {
            ringBuffer.publish(sequence);
        }
    }
}

 

显而易见的是,事件发布变得比使用简单队列更复杂。 这是由于对事件预分配的需求。 它需要(在最低级别)消息发布的两阶段方法,即声明环形缓冲区中的时隙然后发布可用数据。 还必须将发布包装在try / finally块中。 如果我们在Ring Buffer中声明一个插槽(调用RingBuffer.next()),那么我们必须发布这个序列。 如果不这样做可能会导致干扰者状态的腐败。 具体而言,在多生产者的情况下,这将导致消费者停滞并且在没有重启的情况下无法恢复。 因此,建议使用EventTranslator API。

最后一步是将整个事物连接在一起。 可以手动连接所有组件,但是它可能有点复杂,因此提供DSL以简化构造。 一些更复杂的选项不能通过DSL获得,但它适用于大多数情况

public class LongEventMain
{
    public static void main(String[] args) throws Exception
    {
        // The factory for the event
        LongEventFactory factory = new LongEventFactory();

        // Specify the size of the ring buffer, must be power of 2.
        int bufferSize = 1024;

        // Construct the Disruptor
        Disruptor<LongEvent> disruptor = new Disruptor<>(factory, bufferSize, DaemonThreadFactory.INSTANCE);

        // Connect the handler
        disruptor.handleEventsWith(new LongEventHandler());

        // Start the Disruptor, starts all threads running
        disruptor.start();

        // Get the ring buffer from the Disruptor to be used for publishing.
        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();

        LongEventProducer producer = new LongEventProducer(ringBuffer);

        ByteBuffer bb = ByteBuffer.allocate(8);
        for (long l = 0; true; l++)
        {
            bb.putLong(0, l);
            producer.onData(bb);
            Thread.sleep(1000);
        }
    }
}

 

Using Java 8
Disruptor API的设计影响之一是Java 8将依赖于功能接口的概念来充当Java Lambdas的类型声明。 Disruptor API中的大多数接口定义符合功能接口的要求,因此可以使用Lambda而不是自定义类,这可以减少所需的锅炉位置。

public class LongEventMain
{
    public static void main(String[] args) throws Exception
    {
        // Specify the size of the ring buffer, must be power of 2.
        int bufferSize = 1024;

        // Construct the Disruptor
        Disruptor<LongEvent> disruptor = new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);

        // Connect the handler
        disruptor.handleEventsWith((event, sequence, endOfBatch) -> System.out.println("Event: " + event));

        // Start the Disruptor, starts all threads running
        disruptor.start();

        // Get the ring buffer from the Disruptor to be used for publishing.
        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();

        ByteBuffer bb = ByteBuffer.allocate(8);
        for (long l = 0; true; l++)
        {
            bb.putLong(0, l);
            ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
            Thread.sleep(1000);
        }
    }
}

 

注意如何不再需要许多类(例如处理程序,翻译器)。 还要注意用于publishEvent()的lambda如何仅引用传入的参数。如果我们将该代码编写为:

ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
    bb.putLong(0, l);
    ringBuffer.publishEvent((event, sequence) -> event.set(bb.getLong(0)));
    Thread.sleep(1000);
}

 

这将创建一个捕获lambda,这意味着它需要实例化一个对象来保存ByteBuffer bb变量,因为它将lambda传递给publishEvent()调用。 这将产生额外的(不必要的)垃圾,因此如果要求低GC压力,则应首选将参数传递给lambda的调用。
给那个方法引用可以用来代替匿名lamdbas,可以用这种方式重写这个例子。

public class LongEventMain
{
    public static void handleEvent(LongEvent event, long sequence, boolean endOfBatch)
    {
        System.out.println(event);
    }

    public static void translate(LongEvent event, long sequence, ByteBuffer buffer)
    {
        event.set(buffer.getLong(0));
    }

    public static void main(String[] args) throws Exception
    {
        // Specify the size of the ring buffer, must be power of 2.
        int bufferSize = 1024;

        // Construct the Disruptor
        Disruptor<LongEvent> disruptor = new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);

        // Connect the handler
        disruptor.handleEventsWith(LongEventMain::handleEvent);

        // Start the Disruptor, starts all threads running
        disruptor.start();

        // Get the ring buffer from the Disruptor to be used for publishing.
        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();

        ByteBuffer bb = ByteBuffer.allocate(8);
        for (long l = 0; true; l++)
        {
            bb.putLong(0, l);
            ringBuffer.publishEvent(LongEventMain::translate, bb);
            Thread.sleep(1000);
        }
    }
}

 

基本调整选项
使用上述方法将在最广泛的部署方案中起到功能。 但是,如果您能够对Disruptor运行的硬件和软件环境做出某些假设,那么您可以利用许多调优选项来提高性能。 调整有两个主要选项,单个与多个生产者和替代等待策略。

单一与多生产者
提高并发系统性能的最佳方法之一是遵循Single Writer原则,这适用于Disruptor。 如果您处于只有一个线程产生事件进入Disruptor的情况下,那么您可以利用它来获得额外的性能。

public class LongEventMain
{
    public static void main(String[] args) throws Exception
    {
        //.....
        // Construct the Disruptor with a SingleProducerSequencer
        Disruptor<LongEvent> disruptor = new Disruptor(
            factory, bufferSize, ProducerType.SINGLE, new BlockingWaitStrategy(), DaemonThreadFactory.INSTANCE);
        //.....
    }
}

 

为了说明通过这种技术可以实现多少性能优势,我们可以在OneToOne性能测试中更改生产者类型。 测试在i7 Sandy Bridge MacBook Air上运行。
Multiple Producer
Run 0, Disruptor=26,553,372 ops/sec
Run 1, Disruptor=28,727,377 ops/sec
Run 2, Disruptor=29,806,259 ops/sec
Run 3, Disruptor=29,717,682 ops/sec
Run 4, Disruptor=28,818,443 ops/sec
Run 5, Disruptor=29,103,608 ops/sec
Run 6, Disruptor=29,239,766 ops/sec
Single Producer
Run 0, Disruptor=89,365,504 ops/sec
Run 1, Disruptor=77,579,519 ops/sec
Run 2, Disruptor=78,678,206 ops/sec
Run 3, Disruptor=80,840,743 ops/sec
Run 4, Disruptor=81,037,277 ops/sec
Run 5, Disruptor=81,168,831 ops/sec
Run 6, Disruptor=81,699,346 ops/sec

替代等待策略
Disruptor使用的默认等待策略是BlockingWaitStrategy。在内部,BlockingWaitStrategy使用典型的锁和条件变量来处理线程唤醒。 BlockingWaitStrategy是可用等待策略中最慢的,但对于CPU使用率而言是最保守的,并且将在最广泛的部署选项中提供最一致的行为。但是,再次了解已部署的系统可以提供额外的性能。

SleepingWaitStrategy
与BlockingWaitStrategy一样,SleepWaitStrategy通过使用简单的忙等待循环尝试保守CPU使用率,但在循环中间使用对LockSupport.parkNanos(1)的调用。在典型的Linux系统上,这将使线程暂停约60μs。然而,它具有以下好处:生产线程不需要采取任何其他增加适当计数器的动作,并且不需要发信号通知条件变量的成本。但是,在生产者和消费者线程之间移动事件的平均延迟会更高。它在不需要低延迟的情况下效果最好,但是对生产线程的影响很小。常见用例是异步日志记录。

YieldingWaitStrategy
YieldingWaitStrategy是可以在低延迟系统中使用的2种等待策略之一,其中可以选择燃烧CPU周期以改善延迟。 YieldingWaitStrategy将忙于等待序列增加到适当的值。在循环体内,将调用Thread.yield(),允许其他排队的线程运行。当需要非常高的性能并且事件处理程序线程的数量小于逻辑核心的总数(例如,逻辑核心数)时,这是推荐的等待策略。你启用了超线程。

BusySpinWaitStrategy
BusySpinWaitStrategy是性能最高的等待策略,但对部署环境施加了最高限制。仅当事件处理程序线程的数量小于框中的物理核心数时,才应使用此等待策略。例如。应禁用超线程。

从环形缓冲区清除对象
通过Disruptor传递数据时,对象的寿命可能超过预期。 为避免这种情况发生,可能需要在处理后清除事件。 如果您有一个事件处理程序清除同一个处理程序中的值就足够了。 如果您有一系列事件处理程序,那么您可能需要在链的末尾放置一个特定的处理程序来处理清除对象。

class ObjectEvent<T>
{
    T val;

    void clear()
    {
        val = null;
    }
}

public class ClearingEventHandler<T> implements EventHandler<ObjectEvent<T>>
{
    public void onEvent(ObjectEvent<T> event, long sequence, boolean endOfBatch)
    {
        // Failing to call clear here will result in the 
        // object associated with the event to live until
        // it is overwritten once the ring buffer has wrapped
        // around to the beginning.
        event.clear(); 
    }
}

public static void main(String[] args)
{
    Disruptor<ObjectEvent<String>> disruptor = new Disruptor<>(
        () -> ObjectEvent<String>(), bufferSize, DaemonThreadFactory.INSTANCE);

    disruptor
        .handleEventsWith(new ProcessingEventHandler())
        .then(new ClearingObjectHandler());
}

 

posted @ 2018-12-27 09:54  地上有代码  阅读(596)  评论(0编辑  收藏  举报