原文 地址 作者 Trisha 译者:李同杰
LMAX Disruptor 是一个开源的并发框架,并获得2011 Duke’s 程序框架创新奖。本文将用图表的方式为大家介绍Disruptor是什么,用来做什么,以及简单介绍背后的实现原理。
Disruptor是什么?
Disruptor 是线程内通信框架,用于线程里共享数据。LMAX 创建Disruptor作为可靠消息架构的一部分并将它设计成一种在不同组件中共享数据非常快的方法。
基于Mechanical Sympathy(对于计算机底层硬件的理解),基本的计算机科学以及领域驱动设计,Disruptor已经发展成为一个帮助开发人员解决很多繁琐并发编程问题的框架。
很多架构都普遍使用一个队列共享线程间的数据(即传送消息)。图1 展示了一个在不同的阶段中通过使用队列来传送消息的例子(每个蓝色的圈代表一个线程)。
图 1
这种架构允许生产者线程(图1中的stage1)在stage2很忙以至于无法立刻处理的时候能够继续执行下一步操作,从而提供了解决系统中数据拥堵的方法。这里队列可以看成是不同线程之间的缓冲。
在这种最简单的情况下,Disruptor 可以用来代替队列作为在不同的线程传递消息的工具(如图2所示)。
图2
这种数据结构叫着RingBuffer,是用数组实现的。Stage1线程把数据放进RingBuffer,而Stage2线程从RingBuffer中读取数据。
图2 中,可以看到RingBuffer中每格中都有序号,并且RingBuffer实时监测值最大(最新)的序号,该序号指向RingBuffer中最后一格。序号会伴随着越来越多的数据增加进RingBuffer中而增长。
Disruptor的关键在于是它的设计目标是在框架内没有竞争.这是通过遵守single-writer 原则,即只有一块数据可以写入一个数据块中,而达到的。遵循这样的规则使得Disruptor避免了代价高昂的CAS锁,这也使得Disruptor非常快。
Disruptor通过使用RingBuffer以及每个事件处理器(EventProcessor)监测各自的序号从而减少了竞争。这样,事件处理器只能更新自己所获得的序号。当介绍向RingBuffer读取和写入数据时会对这个概念作进一步阐述。
发布到Disruptor
向RingBuffer写入数据需要通过两阶段提交(two-phase commit)。首先,Stage1线程即发布者必须确定RingBuffer中下一个可以插入的格,如图3所示。
图 3
RingBuffer持有最近写入格的序号(图3中的18格),从而确定下一个插入格的序号。
RingBuffer通过检查所有事件处理器正在从RingBuffer中读取的当前序号来判断下一个插入格是否空闲。
图4显示发现了下一个插入格。
图 4
当发布者得到下一个序号后,它可以获得该格中的对象,并可以对该对象进行任意操作。你可以把格想象成一个简单的可以写入任意值的容器。
同时,在发布者处理19格数据的时候,RingBuffer的序号依然是18,所以其他事件处理器将不会读到19格中的数据。
图5表示对象的改动保存进了RingBuffer。
图5
最终,发布者最终将数据写入19格后,通知RingBuffer发布19格的数据。这时,RingBuffer更新序号并且所有从RingBuffer读数据的事件处理器都可以看到19格中的数据。
RingBuffer中数据读取
Disruptor框架中包含了可以从RingBuffer中读取数据的BatchEventProcessor,下面将概述它如何工作并着重介绍它的设计。
当发布者向RingBuffer请求下一个空格以便写入时,一个实际上并不真的从RingBuffer消费事件的事件处理器,将监控它处理的最新的序号并请求它所需要的下一个序号。
图5显示事件处理器等待下一个序号。
图6
事件处理器不是直接向RingBuffer请求序号,而是通过SequenceBarrier向RingBuffer请求序号。其中具体实现细节对我们的理解并不重要,但是下面可以看到这样做的目的很明显。
如图6中Stage2所示,事件处理器的最大序号是16.它向SequenceBarrier调用waitFor(17)以获得17格中的数据。因为没有数据写入RingBuffer,Stage2事件处理器挂起等待下一个序号。如果这样,没有什么可以处理。但是,如图6所示的情况,RingBuffer已经被填充到18格,所以waitFor函数将返回18并通知事件处理器,它可以读取包括直到18格在内的数据,如图7所示。
图7
这种方法提供了非常好的批处理功能,可以在BatchEventProcessor源码中看到。源码中直接向RingBuffer批量获取从下一个序号直到最大可以获得的序号中的数据。
你可以通过实现EventHandler使用批处理功能。在Disruptor性能测试中有关于如何使用批处理的例子,例如FizzBuzzEventHandler。
是低延迟队列?
当然,Disruptor可以被当作低延迟队列来使用。我们对于Disruptor之前版本的测试数据显示了,运行在一个2.2 GHz的英特尔酷睿i7-2720QM处理器上使用Java 1.6.0_25 64位的Ubuntu的11.04三层管道模式架构中,Disruptor比ArrayBlockingQueue快了多少。表1显示了在管道中的每跳延迟。有关此测试的更多详细信息,请参阅Disruptor技术文件。
但是不要根据延迟数据得出Disruptor只是一种解决某种特定性能问题的方案,因为它不是。
更酷的东西
一个有意思的事是Disruptor是如何支持系统组件之间的依赖关系,并在线程之间共享数据时不产生竞争。
Disruptor在设计上遵守single-writer 原则从而实现零竞争,即每个数据位只能被一个线程写入。但是,这不代表你不可以使用多个线程读数据,而这正是Disruptor所支持的。
Disruptor系统的最初设计是为了支持需要按照特定的顺序发生的阶段性类似流水线事件,这种需求在企业应用系统开发中并不少见。图8显示了标准的3级流水线。
图 8
首先,每个事件都被写入硬盘(日志)作为日后恢复用。其次,这些事件被复制到备份服务器。只有在这两个阶段后,系统开始业务逻辑处理。
按顺序执行上次操作是一个合乎逻辑的方法,但是并不是最有效的方法。日志和复制操作可以同步执行,因为他们互相独立。但是业务逻辑必须在他们都执行完后才能执行。图9显示他们可以并行互不依赖。
图 9
如果使用Disruptor,前两个阶段(日志和复制)可以直接从RingBuffer中读取数据。正如图7种的简化图所示,他们都使用一个单一的Sequence Barrier从RingBuffer获取下一个可用的序号。他们记录他们使用过的序号,这样他们知道那些事件已经读过并可以使用BatchEventProcessor批量获取事件。
业务逻辑同样可以从同一个RingBuffer中读取事件,但是只限于前两个阶段已经处理过事件。这是通过加入第二个SequenceBarrier实现的,用它来监控处理日志的事件处理器和复制的事件处理器,当请求最大可读的序号时,它返回两个处理器中较小的序号。
当每个事件处理器都使用SequenceBarrier 来确定哪些事件可以安全的从RingBuffer中读出,那么就从中读出这些事件。
图10
有很多事件处理器都可以从RingBuffer中读取序号,包括日志事件处理器,复制事件处理器等,但是只有一个处理器可以增加序号。这保证了共享数据没有竞争。
如果有多个发布者?
Disruptor也支持多个发布者向RingBuffer写入。当然,因为这样的话必然会发生两个不同的事件处理器写入同一格的情况,这样就会产生竞争。Disruptor提供ClaimStrategy的处理方式应对有多个发布者的情况。
结论
在这里,我已经在总体上介绍了Disruptor框架是如何高性能在线程中共享数据,并简单阐述了它的原理。有关更高级事件处理器以及向RingBuffer申请空间并等待下一个序号等很多策略在这里都没有涉及,Disruptor是开源的,到代码中去搜索吧。
注1:源自Oracle出版的Java杂志,http://www.oracle.com/technetwork/cn/java/javamagazine/index.html
相关推荐
这篇博客“Java线程间数据交换的疑惑”可能探讨了在并发编程中如何有效地共享和同步数据。`volatile`关键字是Java中用于实现线程间通信的一个重要工具,它在多线程环境下起着关键的作用。 首先,我们要理解`...
### 针对Executor框架与线程共享数据的深入探讨 #### 1. Executor框架的重要性与优势 在Java并发编程领域中,Executor框架扮演着一个非常核心的角色。它为开发者提供了一个高效且易于使用的线程池管理方案。下面将...
8. **资源管理**:多线程可能导致资源竞争,因此需要合理地管理资源,避免死锁或竞态条件的发生。使用`lock`关键字或`Monitor`类可以帮助同步对共享资源的访问。 9. **性能优化**:在设计数据加载策略时,可以考虑...
这种设计使得线程间的通信成本较低,因为数据共享无需通过复杂的进程间通信机制。在单个进程中,可以创建多个线程来并发执行不同的任务,提高了处理器的利用率和程序的响应速度。 线程的类型: 1. 用户级线程:...
为了解决这个问题,开发者通常会使用“数据插槽”或“线程间通信”机制来确保数据在不同线程间的正确同步和传递。 在Winform中,主线程负责处理所有的UI更新,而其他线程则可以用于执行耗时的操作,如数据库查询、...
线程共享同一进程的内存空间,这使得线程间的通信更为便捷,但也增加了数据同步的复杂性。 2. **创建线程**:在大多数编程语言中,创建线程可以通过继承线程类或实现Runnable接口来实现。例如,在Java中,可以使用...
共享内存是一种高效的IPC机制,它允许多个进程访问同一块内存区域,无需进行数据复制。这种方式减少了数据传输的时间开销,提高了系统性能。在标题中提到的场景,我们有两个进程,它们都与同一片内存区域交互,以此...
在Linux操作系统中,线程间同步是多线程编程中的一个重要概念,用于确保多个线程在访问共享资源时能够有序进行,避免数据竞争和不一致性。本文将详细讲解Linux线程间同步的各种机制和实现方法。 一、互斥量(Mutex...
通过使用无锁算法,Disruptor能够在多个线程之间共享数据而无需使用传统的锁,这极大地提高了并发性能。 4. **事件处理器链**:Disruptor允许用户定义一系列事件处理器,形成一个处理链。当事件被放入缓冲区后,会...
当多线程环境对SQLite进行读写操作时,可能会引发数据竞争和并发问题,因此必须采取适当的同步策略来确保数据的一致性和完整性。 标题"**C#多线程读写sqlite**"涉及的主要知识点包括: 1. **多线程编程**:C#中的`...
7. **线程安全的数据访问**:如果线程需要访问共享数据,需要确保这些访问是线程安全的。这可能涉及到使用线程安全的数据结构,如线程局部存储(TLS)或互斥量保护的数据访问。 8. **线程的生命周期管理**:线程的...
由于多个线程共享同一进程的内存空间,因此需要确保线程间的正确同步,避免数据竞争和死锁问题。常用的技术包括互斥锁(mutex)、条件变量等。 **3. 线程管理和控制** 除了创建线程外,还需要对线程进行有效的管理...
2. **线程同步**:在多线程环境中,同步是至关重要的,以避免数据竞争和死锁等问题。C#提供了一些同步原语,如Mutex(互斥锁)、Semaphore(信号量)和Monitor(监视器)。Mutex允许一次只有一个线程访问共享资源,...
3. **代码复用性好**:线程之间可以共享数据和状态,减少了代码的冗余。 4. **可扩展性强**:随着硬件性能的提升,多线程应用可以通过增加线程数量来进一步提升性能。 ### 三、多线程编程的关键技术 1. **线程创建...
通过阅读和分析这些源码,开发者可以深入理解易语言如何在底层实现线程管理,如何进行线程同步和通信,以及如何处理线程间的数据共享和竞争问题。源码学习可以帮助开发者更高效地利用多线程,避免常见的并发问题,如...
- **资源共享**:线程之间可以轻松共享进程内的数据和资源,无需显式地通过进程间通信(IPC)机制。 - **响应快速**:线程切换的开销远小于进程,因为它们共享相同的内存空间,这使得系统能更快地响应外部事件。 - *...
1. **线程间通信**:循环队列可以作为线程间的共享数据结构,用于传递消息或者任务。线程A可以将任务放入队列,线程B则从队列中取出并执行。这种方式避免了线程直接访问共享资源,减少了锁的使用,降低了死锁和竞态...
2. 线程局部变量(ThreadLocal):每个线程都有自己独立的副本,避免了线程间的数据共享,从而解决了并发问题。 3. 同步访问:使用synchronized关键字或者Lock接口对共享资源进行保护,确保同一时间只有一个线程能...
4. **线程同步与通信**:在多线程环境中,线程间的同步与通信至关重要,以避免数据竞争和死锁。这通常涉及互斥锁(Mutex)、信号量(Semaphore)、条件变量(Condition Variable)和线程间消息传递等机制。例如,...
- 线程安全:确保对共享资源的访问是线程安全的,避免数据竞争。 - 死锁预防:避免多个线程相互等待对方释放资源导致的死锁现象。 - 资源管理:正确使用`pthread_join()`和`pthread_detach()`来管理线程生命周期,...