How do consumers read data?
The first one deals with producers, and this one deals with consumers.
We all know that consumers are constantly reading and processing data from queues.But unlike BlockedQueue, RingBuffer consumers don't lock queues, so how does it work?
In a nutshell, CAS atomically gives a consumable sequence number, which is then used to extract the data for processing.
Before we look at the code, let's list what we can think of:
1. A tail pointer is needed to track spending
2. How can I prevent one data from being consumed repeatedly by multiple consumers?
3. Consumption rate cannot exceed that of producers. How can I limit it?
4. What should consumers do when there is no data to process, spin or hang up waiting for the producer to wake up?
5.If 4 chooses to hang, how do I wake up the consumer to end the thread task if RingBuffer is off?
6.RingBuffer needs to be constructed with a thread factory passed in. How does RingBuffer use threads and multiple tasks are scheduled with one thread?
7. When will consumers start?
Okay, so now let's look at the code. Here's an implementation of EventProcessor, WorkProcessor's code.
public final class WorkProcessor<T> implements EventProcessor { private final AtomicBoolean running = new AtomicBoolean(false); //Current processor state private final Sequence sequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE); //Latest Sequence Number Currently Consumed private final RingBuffer<T> ringBuffer; //Keep this reference for easy access to data private final SequenceBarrier sequenceBarrier; //For waiting for the next maximum available sequence number, can be used with multiple Processor Share private final WorkHandler<? super T> workHandler; //Actual Processor private final ExceptionHandler<? super T> exceptionHandler; private final Sequence workSequence; //Multiple Processor Shared workSequence,You can get the next sequence number to be processed private final EventReleaser eventReleaser = new EventReleaser() { @Override public void release() { sequence.set(Long.MAX_VALUE); } }; private final TimeoutHandler timeoutHandler; /** * Construct a {@link WorkProcessor}. * * @param ringBuffer to which events are published. * @param sequenceBarrier on which it is waiting. * @param workHandler is the delegate to which events are dispatched. * @param exceptionHandler to be called back when an error occurs * @param workSequence from which to claim the next event to be worked on. It should always be initialised * as {@link Sequencer#INITIAL_CURSOR_VALUE} */ public WorkProcessor( final RingBuffer<T> ringBuffer, final SequenceBarrier sequenceBarrier, final WorkHandler<? super T> workHandler, final ExceptionHandler<? super T> exceptionHandler, final Sequence workSequence) { this.ringBuffer = ringBuffer; this.sequenceBarrier = sequenceBarrier; this.workHandler = workHandler; this.exceptionHandler = exceptionHandler; this.workSequence = workSequence; if (this.workHandler instanceof EventReleaseAware) { ((EventReleaseAware) this.workHandler).setEventReleaser(eventReleaser); } timeoutHandler = (workHandler instanceof TimeoutHandler) ? (TimeoutHandler) workHandler : null; } @Override public Sequence getSequence() { return sequence; } @Override public void halt() { running.set(false); sequenceBarrier.alert(); //Wake-up card in WaitStrategy Of Processor Thread so that it knows the End state } /** * remove workProcessor dynamic without message lost */ public void haltLater() { running.set(false); //So-called later,Is to wait for the next check before launching, if stuck in WaitStrategy,Wait for it to return before checking } @Override public boolean isRunning() { return running.get(); } /** * It is ok to have another thread re-run this method after a halt(). * * @throws IllegalStateException if this processor is already running */ @Override public void run() { if (!running.compareAndSet(false, true)) //Prevent run Problems caused by duplicate method calls { throw new IllegalStateException("Thread is already running"); } sequenceBarrier.clearAlert(); notifyStart(); boolean processedSequence = true; long cachedAvailableSequence = Long.MIN_VALUE; long nextSequence = sequence.get(); T event = null; while (true) //Dead cycle { try { // if previous sequence was processed - fetch the next sequence and set // that we have successfully processed the previous sequence // typically, this will be true // this prevents the sequence getting too far forward if an exception // is thrown from the WorkHandler if (processedSequence) { if (!running.get()) //If it is checked that it is off, wake up in the same Barrier Other on processor thread { sequenceBarrier.alert(); //Wake up other threads sequenceBarrier.checkAlert(); //Throw an exception to terminate this thread } processedSequence = false; do { //workSequence Possible and Multiple Processor Share nextSequence = workSequence.get() + 1L; //this sequence This is the sequence number that the current processor has processed. The producer uses this to determine the tail pointer. This is gatingSequence //When you get the next new number, explain workSequence Previous data has been processed sequence.set(nextSequence - 1L); } //Because workSequence May be by multiple Processor Shared, so there is competition, need to use CAS while (!workSequence.compareAndSet(nextSequence - 1L, nextSequence)); } //If no producer's maximum cursor is exceeded, the data is preferable if (cachedAvailableSequence >= nextSequence) { //Remove the data for the corresponding position of the serial number event = ringBuffer.get(nextSequence); //Hand handler Handle workHandler.onEvent(event); processedSequence = true; } else { //Blocking waiting for next available sequence number //If so nextSequence,Just go back nextSequence //If larger than is available nextSequence,Return to the latest available sequence cachedAvailableSequence = sequenceBarrier.waitFor(nextSequence); } } catch (final TimeoutException e) { notifyTimeout(sequence.get()); } catch (final AlertException ex) //checkAlert()Thrown { if (!running.get()) //If it has already ended, the loop is terminated, and the thread task is terminated { break; } } catch (final Throwable ex) //Other exceptions are handled by the exception handler { // handle, mark as processed, unless the exception handler threw an exception exceptionHandler.handleEventException(ex, nextSequence, event); processedSequence = true; } } notifyShutdown(); running.set(false); } private void notifyTimeout(final long availableSequence) { try { if (timeoutHandler != null) { timeoutHandler.onTimeout(availableSequence); } } catch (Throwable e) { exceptionHandler.handleEventException(e, availableSequence, null); } } private void notifyStart() { if (workHandler instanceof LifecycleAware) { try { ((LifecycleAware) workHandler).onStart(); } catch (final Throwable ex) { exceptionHandler.handleOnStartException(ex); } } } private void notifyShutdown() { if (workHandler instanceof LifecycleAware) { try { ((LifecycleAware) workHandler).onShutdown(); } catch (final Throwable ex) { exceptionHandler.handleOnShutdownException(ex); } } } }
Target Question One: Need a tail pointer to track spending
You notice that there are two Sequences, workSequence and sequence in the code.Why do I need two?
The latest serial number used by the workSequence consumer (the data for that number has not been processed, but has been marked as consumable by the consumer); the data for the sequence number has been consumed, which is the gatingSequence in the previous article.
Question 2: How can I prevent one data from being consumed repeatedly by multiple consumers?
The solution to Problem 2 is WorkPool, where multiple WorkProcessor s share a workSequence so that they compete for a sequence number, which can only be consumed once.
public final class WorkerPool<T> { private final AtomicBoolean started = new AtomicBoolean(false); private final Sequence workSequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE); //from-1 start private final RingBuffer<T> ringBuffer; //RingBuffer Reference, used to construct Processor,Get data // WorkProcessors are created to wrap each of the provided WorkHandlers private final WorkProcessor<?>[] workProcessors; //... public WorkerPool( final RingBuffer<T> ringBuffer, final SequenceBarrier sequenceBarrier, final ExceptionHandler<? super T> exceptionHandler, final WorkHandler<? super T>... workHandlers) { this.ringBuffer = ringBuffer; final int numWorkers = workHandlers.length; workProcessors = new WorkProcessor[numWorkers]; //each handler Construct a Processor for (int i = 0; i < numWorkers; i++) { workProcessors[i] = new WorkProcessor<>( ringBuffer, sequenceBarrier, //Share the same sequenceBarrier workHandlers[i], exceptionHandler, workSequence); //Share the same workSequence } } //... } public class Disruptor<T> { private final RingBuffer<T> ringBuffer; private final Executor executor; private final ConsumerRepository<T> consumerRepository = new ConsumerRepository<>(); private final AtomicBoolean started = new AtomicBoolean(false); private ExceptionHandler<? super T> exceptionHandler = new ExceptionHandlerWrapper<>(); //... public final EventHandlerGroup<T> handleEventsWithWorkerPool(final WorkHandler<T>... workHandlers) { return createWorkerPool(new Sequence[0], workHandlers); } EventHandlerGroup<T> createWorkerPool( final Sequence[] barrierSequences, final WorkHandler<? super T>[] workHandlers) { final SequenceBarrier sequenceBarrier = ringBuffer.newBarrier(barrierSequences); final WorkerPool<T> workerPool = new WorkerPool<>(ringBuffer, sequenceBarrier, exceptionHandler, workHandlers); consumerRepository.add(workerPool, sequenceBarrier); //take workPool Save in Repository,Start from Repository Remove from and give to Executor Handle final Sequence[] workerSequences = workerPool.getWorkerSequences(); updateGatingSequencesForNextInChain(barrierSequences, workerSequences); return new EventHandlerGroup<>(this, consumerRepository, workerSequences); } //.... }
Questions 3 and 4: How to limit the consumption rate if it cannot exceed that of the producers?What should consumers do when there is no data to process, spin or hang up waiting for the producer to wake up?
Using SequenceBarrier, we know from WorkProcessor's code that consumers cache the last maximum consumable number they got, and then compete directly within that range.Each time the minimum available sequence number is obtained, the waitStrategy wait policy is triggered to wait.
There are many wait policies, most commonly BlockingWaitStategy, that suspend execution threads.When the producer publishEvent, the WaitStrategy#signalAllWhenBlocking() method is called to wake up all waiting threads.
final class ProcessingSequenceBarrier implements SequenceBarrier { private final WaitStrategy waitStrategy; private final Sequence dependentSequence; private volatile boolean alerted = false; private final Sequence cursorSequence; private final Sequencer sequencer; //... public long waitFor(final long sequence) throws AlertException, InterruptedException, TimeoutException { checkAlert(); long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this); if (availableSequence < sequence) { return availableSequence; } return sequencer.getHighestPublishedSequence(sequence, availableSequence); } //... }
For questions 6 and 7: When RingBuffer is constructed, it needs to pass in a thread factory. How does RingBuffer use threads? Multiple tasks are scheduled using one thread?When will consumers start?
The consumer starts with Disruptor, which takes the Consumer out of the Consumer Repository and submits it to Executor for execution.
public RingBuffer<T> start() { checkOnlyStartedOnce(); for (final ConsumerInfo consumerInfo : consumerRepository) { consumerInfo.start(executor); } return ringBuffer; }
In the new version of Disruptor, it is not recommended to use an external incoming Executor, but to pass only the ThreadFactory, and then construct an Executor internally, BasicExecutor.Its implementation is to create a new thread responsible for each task submitted.So its threading model is one consumer and one thread.
public class Disruptor<T> { //... public Disruptor(final EventFactory<T> eventFactory, final int ringBufferSize, final ThreadFactory threadFactory) { this(RingBuffer.createMultiProducer(eventFactory, ringBufferSize), new BasicExecutor(threadFactory)); } //... } public class BasicExecutor implements Executor { private final ThreadFactory factory; private final Queue<Thread> threads = new ConcurrentLinkedQueue<>(); public BasicExecutor(ThreadFactory factory) { this.factory = factory; } @Override public void execute(Runnable command) { //Create a new thread to process each task submitted final Thread thread = factory.newThread(command); if (null == thread) { throw new RuntimeException("Failed to create thread to run: " + command); } thread.start(); threads.add(thread); } //... }
//TODO Subsequent collation adds some legends for easy understanding