Deep Interpretation of Disruptor

Keywords: Java Netty less

To optimize the performance of the system to the extreme is always a direction for program enthusiasts. In the field of java concurrency, there are also many practices and innovations, ranging from optimistic locking, CAS, to netty thread model, fibre Quasar, kilim and so on. Disruptor is a lightweight, high-performance concurrency framework that has attracted wide attention for its amazing throughput. Disruptor provides many new ideas to improve the concurrency performance of programs, such as:

  1. Cache line filling eliminates pseudo-sharing;
  2. RingBuffer lock-free queue design;
  3. Pre-allocation of cached objects, the use of cached cyclic coverage instead of cached new deletions, etc.

Next, we will analyze the implementation principle of Disruptor from the point of view of source code.

1 Disruptor terminology

Disruptor has many concepts of its own, which makes it difficult for beginners to read code. So before delving into the principles of Disruptor, you need to understand the main core classes or interfaces of Disruptor.

  • Sequence: A layer of long type wrapped in the form of cached row filling to represent the sequence number of events. Through unsafe cas method, the overhead of lock is avoided.
  • Sequencer: The bridge between producer and cache RingBuffer. Single producer and multi producer correspond to two implementations of Single Producer Sequencer and Multiproducer Sequencer respectively. Sequencer is used to apply for space from RingBuffer, using publish method to notify all Sequence Barriers waiting for consumable events through waitStrategy.
  • WaitStrategy: WaitStrategy has several implementations to express consumers'waiting strategies when there are no consumable events.
  • Sequence Barrier: A bridge between consumers and caching RingBuffer. Consumers do not directly access RingBuffer, which can reduce concurrent conflicts on RingBuffer.
  • Event Processor: Event Processor is the dispatching unit of Executor, a consumer thread pool. It is a layer encapsulation of Event Handler and Exception Handler.
  • Event: Consumer events. The concrete implementation of Event is defined by users.
  • RingBuffer: An array-based cache implementation is also an entry point for creating a sequence and defining a WaitStrategy.
  • Disruptor: Use entry for Disruptor. Hold references such as RingBuffer, Executor, Consumer Repository, etc.

2 Disruptor Source Code Analysis

2.1 Disruptor concurrency model

A typical scenario in the concurrent domain is the producer-consumer model. The conventional way is to use queue as a method of sharing data between producer threads and consumer threads. The competition of read-write locks is inevitable for queue. Disruptor uses Ring Buffer as a medium for sharing data. Producers control RingBuffer through Sequencer and wake up consumers waiting for events. Consumers monitor RingBuffer's consumable events through Sequence Barrier. Consider a scenario where producer A and three consumers B, C, D are involved, and event handling of D requires that B and C complete first. Then the structure of the model is as follows:

Under this structure, each consumer has its own event number Sequence, and there is no shared race among consumers. Sequence Barrier 1 monitors Ring Buffer's serial number cursor, and consumers B and C wait for consumable events through Sequence Barrier 1. Sequence Barrier 2 not only monitors cursor, but also monitors the serial number Sequence of B and C, thus returning the smallest serial number to consumer D, thus realizing the logic that D depends on B and C.
Ring Buffer is a highlight of Disruptor's high performance. RingBuffer is a large array where events are written in a circular override. and Routine Ring Buffer There are two head and tail pointers in different ways. The RingBuffer of Disruptor has only one pointer (or ordinal number) pointing to the next writable position in the array. The ordinal number in the Disruptor source code is cursor in Sequencer. The producer controls the writing of RingBuffer through Sequencer. To avoid write coverage of unexpended events, Sequencer needs to monitor the progress of message processing for all consumers, that is, gating Sequences. RingBuffer implements the lock-free design of event caching in this way.
Next, we will analyze the source code to understand the implementation principle of Disruptor.

2.2 Disruptor class

Disruptor class is the general entry point of Disruptor framework. It can organize the relationship chain of consumers in the form of DSL, and provide methods such as getting events, publishing events, etc. It contains the following attributes:

private final RingBuffer<T> ringBuffer;
/**Consumer Event Processing Thread Pool**/
private final Executor executor;
/**Consumer aggregation**/
private final ConsumerRepository<T> consumerRepository = new ConsumerRepository<T>();
/**Disruptor Whether or not to start the label can only be started once**/
private final AtomicBoolean started = new AtomicBoolean(false);
/**Consumer Event Anomaly Handling Method**/
private ExceptionHandler<? super T> exceptionHandler = new ExceptionHandlerWrapper<T>();

The process of instantiating Disruptor is the process of instantiating RingBuffer and Executor of consumption thread pool. In addition, the most important role of the Disruptor class is to register consumers, the handleEventsWith method. This approach has multiple implementations, and each consumer will eventually be packaged as an Event Processor. Create Event Processors is an important function of packaging consumers.

EventHandlerGroup<T> createEventProcessors(final Sequence[] barrierSequences,
                                           final EventHandler<T>[] eventHandlers)
{
    checkNotStarted();
    //Each consumer has its own event number Sequence
    final Sequence[] processorSequences = new Sequence[eventHandlers.length];   
    //Consumers wait for consumable events through Sequence Barrier
    final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);    for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
    {
        final EventHandler<T> eventHandler = eventHandlers[i];
        //Each consumer is scheduled as a Batch Event Processor
        final BatchEventProcessor<T> batchEventProcessor = new BatchEventProcessor<T>(ringBuffer, barrier, eventHandler);  
        if (exceptionHandler != null)
        {
            batchEventProcessor.setExceptionHandler(exceptionHandler);
        }
        consumerRepository.add(batchEventProcessor, eventHandler, barrier);
        processorSequences[i] = batchEventProcessor.getSequence();
    }
    
    if (processorSequences.length > 0)
    {
        consumerRepository.unMarkEventProcessorsAsEndOfChain(barrierSequences);
    }
    
    return new EventHandlerGroup<T>(this, consumerRepository, processorSequences);
}

From the program, we can see that every consumer is scheduled in the form of BatchEvent Processor, that is to say, the logic of consumers is in BatchEvent Processor.

2.3 EventProcessor

EventProcessor has two implementation classes with operational logic, BatchEventProcessor and WorkProcessor. The processing logic is very similar. Here only BatchEventProcessor is analyzed.
The constructor of BatchEventProcessor uses Data Provider instead of RingBuffer directly. It may be that Disruptor considers leaving room for users to replace RingBuffer event storage. After all, RingBuffer is memory-level.
When the Disruptor starts, the start method of each consumer ConsumerInfo (in the consumer repository collection) is invoked, and eventually runs to the run method of the BatchEventProcessor.

@Override
public void run()
{
    if (!running.compareAndSet(false, true))
    {
        throw new IllegalStateException("Thread is already running");
    }
    sequenceBarrier.clearAlert();
    
    notifyStart();
    
    T event = null;
    // sequence.get() indicates the sequence number currently processed
    long nextSequence = sequence.get() + 1L;
    try
    {
        while (true)
        {
            try
            {
                // The most important function of sequenceBarrier is to keep consumers waiting for the next available serial number.
                // Available serial numbers may be larger than nextSequence, allowing consumers to handle multiple events at a time
                // If the consumer also relies on other consumers, it will return to the smallest one.
                final long availableSequence = sequenceBarrier.waitFor(nextSequence);
                if (nextSequence > availableSequence)
                {
                    Thread.yield();
                }
                
                while (nextSequence <= availableSequence)
                {
                    event = dataProvider.get(nextSequence);
                    // EvetHandler is user-defined event consumption logic
                    eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
                    nextSequence++;
                }
                
                // Tracking events you handle
                sequence.set(availableSequence);
            }
            catch (final TimeoutException e)
            {
                notifyTimeout(sequence.get());
            }
            catch (final AlertException ex)
            {
                if (!running.get())
                {
                    break;
                }
            }
            catch (final Throwable ex)
            {
                exceptionHandler.handleEventException(ex, nextSequence, event);
                sequence.set(nextSequence);
                nextSequence++;
            }
        }
    }
    finally
    {
        notifyShutdown();
        running.set(false);
    }
}

Consumer logic is to query consumable events in a while loop and process them by user-defined consumer logic EvetHandler. The logic for querying consumable events is in SequenceBarrier.

2.4 SequenceBarrier

SequenceBarrier has only one implementation, Processing SequenceBarrier. The following is the constructor of Processing Sequence Barrier.

public ProcessingSequenceBarrier(final Sequencer sequencer,final WaitStrategy waitStrategy,final Sequence cursorSequence,final Sequence[] dependentSequences)
{
    // Producer's ring Buffer controller sequencer
    this.sequencer = sequencer;
    // Strategies for Consumers to Wait for Consumable Events
    this.waitStrategy = waitStrategy;
    // cursor of ringBuffer
    this.cursorSequence = cursorSequence;
    if (0 == dependentSequences.length)
    {
        dependentSequence = cursorSequence;
    }
    else
    {
    // When relying on other consumers, dependentSequence is the serial number of other consumers.
        dependentSequence = new FixedSequenceGroup(dependentSequences);
    }
}

Consumers wait for the consumable serial number through the waitFor method of Processing Sequence Barrier, which actually calls the waitFor method of WaitStrategy.

2.5 WaitStrategy

WaitStrategy has six implementation classes that represent six different waiting strategies, such as blocking, busy, and so on. Here we analyze only one blocking strategy, Blocking Wait Strategy.

@Override
public long waitFor(long sequence, Sequence cursorSequence, Sequence dependentSequence, SequenceBarrier barrier)
    throws AlertException, InterruptedException
{
    long availableSequence;
    if ((availableSequence = cursorSequence.get()) < sequence)
    {
        lock.lock();
        try
        {
            // If the cursor of ringBuffer is less than the required serial number, that is, the producer does not emit new events, the consumer thread is blocked until the producer wakes up the consumer through Sequencer's publish method.
            while ((availableSequence = cursorSequence.get()) < sequence)
            {
                barrier.checkAlert();
                processorNotifyCondition.await();
            }
        }
        finally
        {
            lock.unlock();
        }
    }
    
    // If the producer publishes a new event, but the other consumers who depend on it are not finished, they wait for the dependent consumers to deal with it first. In this example, D can handle events only after B and C are processed first.
    while ((availableSequence = dependentSequence.get()) < sequence)
    {
        barrier.checkAlert();
    }
    
    return availableSequence;
}

At this point, the consumer's procedural logic is basically clear. Finally, take a look at the producer's procedural logic, mainly Sequencer.

2.6 Sequencer

Sequencer is responsible for the producer's control over RingBuffer, including querying whether there is write space, application space, publishing events and waking up consumers. Sequencer has two implementations, Single Producer Sequencer and Multiproducer Sequencer, which correspond to single producer model and multi producer model respectively. As long as you understand hasAvailable Capacity (), the application space will be clear. The following is the hasAvailable Capacity implementation of Single Producer Sequencer.

@Override
public boolean hasAvailableCapacity(final int requiredCapacity)
{
    long nextValue = pad.nextValue;
    // wrapPoint is a critical serial number and must be smaller than the current minimum unconsumed serial number
    long wrapPoint = (nextValue + requiredCapacity) - bufferSize;
    // Current Minimum Unconsumed Number
    long cachedGatingSequence = pad.cachedValue;
    
    if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
    {
        long minSequence = Util.getMinimumSequence(gatingSequences, nextValue);
        pad.cachedValue = minSequence;
        
        if (wrapPoint > minSequence)
        {
            return false;
        }
    }
    return true;
}

3 Disruptor instance

This example is based on 3.2.0 version of Disruptor, and implements the concurrent scenario described by 2.1 summary. The process of using Disruptor is very simple. It only takes a few simple steps.
Define user events:

public class MyEvent {
    private long value;
    
    public MyEvent(){}
    
    public long getValue() {
        return value;
    }
    
    public void setValue(long value) {
        this.value = value;
    }
}

Define the event factory, which is needed to instantiate Disruptor:

public class MyEventFactory implements EventFactory<MyEvent> {
    public MyEvent newInstance() {
        return new MyEvent();
    }
}

Define consumers B, C, D:

public class MyEventHandlerB implements EventHandler<MyEvent> {
    public void onEvent(MyEvent myEvent, long l, boolean b) throws Exception {
        System.out.println("Comsume Event B : " + myEvent.getValue());
    }
}

public class MyEventHandlerC implements EventHandler<MyEvent> {
    public void onEvent(MyEvent myEvent, long l, boolean b) throws Exception {
        System.out.println("Comsume Event C : " + myEvent.getValue());
    }
}

public class MyEventHandlerD implements EventHandler<MyEvent> {
    public void onEvent(MyEvent myEvent, long l, boolean b) throws Exception {
        System.out.println("Comsume Event D : " + myEvent.getValue());
    }
}

On this basis, you can run Disruptor:

public static void main(String[] args){
    EventFactory<MyEvent> myEventFactory = new MyEventFactory();
    Executor executor = Executors.newCachedThreadPool();
    int ringBufferSize = 32;
    
    Disruptor<MyEvent> disruptor = new Disruptor<MyEvent>(myEventFactory,ringBufferSize,executor, ProducerType.SINGLE,new BlockingWaitStrategy());
    EventHandler<MyEvent> b = new MyEventHandlerB();
    EventHandler<MyEvent> c = new MyEventHandlerC();
    EventHandler<MyEvent> d = new MyEventHandlerD();
    
    SequenceBarrier sequenceBarrier2 = disruptor.handleEventsWith(b,c).asSequenceBarrier();
    BatchEventProcessor processord = new BatchEventProcessor(disruptor.getRingBuffer(),sequenceBarrier2,d);
    disruptor.handleEventsWith(processord);
//  Disruptor. after (b, c). handleEvents With (d); // This line can replace the program logic of the previous two lines.
    RingBuffer<MyEvent> ringBuffer = disruptor.start();    // Start Disruptor
    for(int i=0; i<10; i++) {
        long sequence = ringBuffer.next();                 // Application location
        try {
            MyEvent myEvent = ringBuffer.get(sequence);
            myEvent.setValue(i);                           // Placing data
        } finally {
            ringBuffer.publish(sequence);                  // Submit, if you do not submit the completion event, it will always block
        }
        try{
            Thread.sleep(100);
        }catch (Exception e){
        }
    }
    disruptor.shutdown();
}

According to the logic of the program, B and C will take the lead in dealing with the events in ringBuffer, and the order of processing is irrespective. The same event is processed by B and C before it is processed by D. The results are as follows:

Comsume Event C : 0
Comsume Event B : 0
Comsume Event D : 0
Comsume Event C : 1
Comsume Event B : 1
Comsume Event D : 1
Comsume Event C : 2
Comsume Event B : 2
Comsume Event D : 2
Comsume Event C : 3
Comsume Event B : 3
Comsume Event D : 3
Comsume Event C : 4
Comsume Event B : 4
Comsume Event D : 4
Comsume Event C : 5
Comsume Event B : 5
Comsume Event D : 5
Comsume Event C : 6
Comsume Event B : 6
Comsume Event D : 6
Comsume Event C : 7
Comsume Event B : 7
Comsume Event D : 7
Comsume Event C : 8
Comsume Event B : 8
Comsume Event D : 8
Comsume Event C : 9
Comsume Event B : 9
Comsume Event D : 9

By removing Thread.sleep from this example, it can be observed that B and C are processed in the same order, and the results are in line with expectations.

This article is the author's original, reproduced please indicate the source. http://www.cnblogs.com/miao-rui/p/6379473.html

Posted by shaoen01 on Sun, 24 Mar 2019 04:45:30 -0700