Distuptor's high-performance lock-free framework

Introduction to Distuptor

First introduction to Distuptor

It is a high-performance lock-free framework suitable for high-concurrency business scenarios. In fact, it is a producer-consumer model internally, somewhat similar to the thread-safe SPSC queue.

The following is its basic concept

It is a high-performance lock-free concurrency framework that uses a ring buffer as a message queue to enable efficient data exchange and event processing

RingBuffer——Disruptor's underlying data structure implementation, core class, is the transfer place for exchanging data between threads;

Sequencer - Sequence manager, the implementer of production synchronization, responsible for the management and coordination of consumer/producer serial numbers and serial number fences. Sequencer has two different modes: single producer and multi-producer, which implement various synchronizations. algorithm;

Sequence - Sequence number, declares a sequence number, used to track changes in tasks in the ringbuffer and consumer consumption. Most of the concurrent code in the disruptor is implemented by synchronously modifying the value of the Sequence, rather than locking. This is the disruptor A major reason for high performance;

SequenceBarrier - Sequence number barrier, manages and coordinates the cursor sequence number of the producer and the sequence number of each consumer, ensuring that the producer does not overwrite messages that the consumer will not be able to process in the future, and ensures that dependent consumers can be processed in the correct order.

EventProcessor——Event processor, listens to RingBuffer events and consumes available events. Events read from RingBuffer will be handed over to the actual producer implementation class for consumption; it will keep listening to the next available sequence number until the sequence number corresponds The event is ready.

EventHandler - business processor, is the interface of the actual consumer, completes the implementation of specific business logic, and a third party implements the interface; representing the consumer.

Producer - producer interface, a third-party thread plays this role, and the producer writes events to the RingBuffer.

Wait Strategy: Wait Strategy determines how a consumer waits for the producer to put the event into the Disruptor.


data structure

RingBuffer The bottom layer of the ring buffer is a spatially continuous array. In addition to the array, there is also a sequence number to point to the next available element for use by producers and consumers.

as the picture shows:
Insert image description here


wait strategy

Here are its four waiting strategies

The BlockingWaitStrategy
blocking wait strategy is the Disruptor's default strategy. Inside BlockingWaitStrategy, locks and conditions are used to control thread wake-up. When a consumer tries to fetch data from the Disruptor, if no data is available, it will block until new data is available or it times out. BlockingWaitStrategy is the least efficient strategy, but it consumes the least CPU and provides more consistent performance across various deployment environments.

SleepingWaitStrategy
sleep waiting strategy, SleepingWaitStrategy uses a loop plus the Thread.sleep() method to give up CPU resources during the waiting period, reducing CPU usage during busy waiting. When no data is available, the consumer thread will spin and wait for a period of time, then sleep for a short period of time, and try to obtain data again. It is suitable for scenarios with high latency requirements, because it can reduce CPU usage to a certain extent, but it may also cause a certain increase in latency.

YieldingWaitStrategy
is one of the strategies that can be used in low-latency systems. During the waiting period, use the Thread.yield() method to give up CPU resources and give up execution opportunities to other threads. This strategy is recommended in scenarios where extremely high performance is required and the number of event processing threads is smaller than the number of CPU logical cores

The BusySpinWaitStrategy
has the best performance and is suitable for low-latency systems. Use a loop to continuously try to get data without giving up CPU resources. This strategy is recommended in scenarios that require extremely high performance and the number of event processing threads is smaller than the number of logical cores of the CPU; for example, the CPU enables hyper-threading.

PhasedBackoffWaitStrategy
uses spin + yield + custom strategy and is used in scenarios where CPU resources are tight and throughput and latency are not important.

Generally speaking, BlockingWaitStrategy is suitable for scenarios with low performance requirements, SleepingWaitStrategy is suitable for scenarios that are relatively sensitive to latency, YieldingWaitStrategy is suitable for scenarios with low latency and high concurrency, and BusySpinWaitStrategy is suitable for scenarios with very high latency requirements and high concurrency. scene


DistuptorPerformance

Disruptor's latency and throughput are much better than ArrayBlockingQueue.
The reason why it has better performance than several queues built into jdk is that it adopts the lock-free strategy of cas, and it is very friendly to the CPU.

preallocated memory

It creates a spatially continuous ring buffer at the beginning to store ring queues and event objects, improves data locality and cache hit rate, and avoids subsequent memory applications and memory releases.

Use cpu-cache

Generally, our code is at the memory level, and Distuptor uses the cache area cpu-cache. The read and write speeds of the two are hundreds of times different. The
buffer line filling strategy is used to ensure that the data is always in the cpu-cache and will not be written to the main memory. , enjoy the high-speed reading and writing of the cpu cache

Cache line filling
CPU cache reads and writes in units of cache line (Cache Line), usually 64 bytes. Disruptor uses the concept of cache line filling to ensure that related data structures are in the same cache line, preventing multiple threads from modifying different data in the same cache line at the same time, thereby reducing the phenomenon of false sharing of cache lines.

Disruptor will perform cache line alignment on event objects to ensure that each event object occupies the entire cache line . The purpose of this is to avoid multiple event objects in the same cache line and reduce the phenomenon of false sharing of cache lines.

data structure

The array structure is suitable for the multi-stage pipeline of the CPU and the branch prediction of the CPU, reducing the time cost of subsequent operation execution.


Distuptor use

Distuptor configuration steps

1. Define the event object: First, you need to define an event object, which contains data that needs to be passed between different threads.

public class YourEvent {
    // 定义事件数据的成员变量
}

2. Define event handlers (consumers): Create one or more event handlers to process event objects.

public class YourEventHandler implements EventHandler<YourEvent> {
    public void onEvent(YourEvent event, long sequence, boolean endOfBatch) {
        // 处理事件数据
    }
}

3. Create a Disruptor instance: Use the create method of Disruptor to create a Disruptor instance, and set the size of the ring buffer and the event factory.

int bufferSize = 1024;
Disruptor<YourEvent> disruptor = new Disruptor<>(YourEvent::new, bufferSize, Executors.defaultThreadFactory());

4. Connect the event handler: Connect the event handler to the Disruptor.

disruptor.handleEventsWith(new YourEventHandler());

5. Start the Disruptor: Use the start method to start the Disruptor and start event processing.

disruptor.start();

6. Publish events: Publish events through the publishEvent method of Disruptor and send event data to the ring buffer.

RingBuffer<YourEvent> ringBuffer = disruptor.getRingBuffer();
long sequence = ringBuffer.next();
YourEvent event = ringBuffer.get(sequence);
// 设置事件数据
ringBuffer.publish(sequence);

Single/Multiple Producer Strategy

Use a single consumer:

The simplest way is to use only one consumer thread to consume messages. This ensures that messages are processed in the order published by the producer, since only one consumer is processing the messages.

Use multiple consumers:

However, the order of messages processed by each consumer remains consistent.
If you need to use multiple consumer threads to process messages in parallel, but still want to maintain the order of messages, you can use the following strategy:

  • Use Disruptor's WorkProcessor to create consumer threads. WorkProcessor will ensure that each consumer thread only processes its own independent message sequence and maintains the order of messages. This way, each consumer thread can process messages independently without competing with other consumer threads.
  • Add identifiers such as sequence numbers or timestamps to messages, and consumers can sort them based on these identifiers when processing messages. After receiving the messages, the consumer can sort them according to the identifier to ensure that the messages are processed in the specified order.

Guess you like

Origin blog.csdn.net/giveupgivedown/article/details/132503213