Disruptor介绍(二)

来源:互联网 发布:公司网络维护 编辑:程序博客网 时间:2024/06/07 18:19
转载自:http://www.cnblogs.com/haiq/p/4112689.html
             http://www.zijinxing.com/201501/130218.html
             http://www.th7.cn/Program/java/201410/289634.shtml

五、Disruptor其它性能提高点

       Disruptor还有什么提高性能的地方呢?下面列举一下除了无锁编程外的其他性能优化点。
       1.缓存行填充(Cache Line Padding):CPU缓存常以64bytes作为一个缓存行大小,缓存由若干个缓存行组成,缓存写回主存或主存写入缓存均是以行为单位,此外每个 CPU核心都有自己的缓存(但是若某个核心对某缓存行做出修改,其他拥有同样缓存的核心需要进行同步),生产者和消费者的指针用long型表示,假设现在只有一个生产者和一个消费者,那么双方的指针间没有什么直接联系,只要不“挨着”,应该可以各改各的指针。

下面问题来了: 如果生产者和消费者的指针(加起来共16bytes)出现在同一个缓存行中会怎么样?例如CPU核心A运行的消费者修改了一下自己的指针值(P1),那么其他核心中所有缓存了P1的缓存行都将失效,并从主存重新调配。这样做的缺点显而易见,但是CPU和编译器并未聪明到避免这个问题,所以需要缓存行填充。虽然问题产生的原因很绕,但是解决方案却非常简单:对于一个long型的缓冲区指针,用一个长度为8的long型数组代替。如此一来,一个缓存行被这个数 组填充满,线程对各自指针的修改不会干扰到他人。


       2.避免GC:写Java程序的时候,new各种对象,虽然Java的GC会负责回收,但是系统在高压力情况下频繁的new必定导致更频繁的GC,Disruptor避免这个问题的策略是:提前分配。在创建RingBuffer实例时,参数中要求给出缓冲区元素类型的Factory,创建实例时,Ring Buffer会首先将整个缓冲区填满为Factory所产生的实例,后面生产者生产时,不再用传统做法(顺手new一个实例出来然后add到buffer 中),而是获得之前已经new好的实例,然后设置其中的值。举个形象的例子就是,若缓冲区是个放很多纸片的地方,纸片上记录着信息,以前的做法是:每次加入缓冲区时,都从系统那现准备一张纸片,然后再写好纸片放进缓冲区,消费完就随手扔掉。现在的做法是:实现准备好所有的纸片,想放入时只需要擦掉原来的信息写上新的即可。


       3.成批操作(Batch):Ring Buffer的核心操作是生产和消费,如果能减少这两个操作的次数,性能必然相应地提高。Disruptor中使用成批操作来减少生产和消费的次数,下面具体说一下Disruptor的生产和消费过程中如何体现Batch的。向RingBuffer生产东西的时候,需要经过2个阶段:阶段一为申请空间,申请后生产者获得了一个指针范围[low,high],然后再对缓冲区中[low,high]这段的所有对象进行setValue,阶段2为发布(像这样ringBuffer.publish(low,high);)。阶段1结束后,其他生产者再申请的话,会得到另一段缓冲区。阶段2结束后,之前申请的这一段数据就可以被消费者读到。Disruptor推荐成批生产、成批发布,减少生产时的同步带来的性能损失。从RingBuffer消费东西的时候也需要两个阶段,阶段一为等待生产者的(写)指针值超过指定值(N,即N之前的数据已经消费过了),阶段一执行完后,消费者会得到一个指针值 (R),表示Ring Buffer中下标R之前的值是可以读的。阶段2就是具体读取(略)。阶段一返回值R很有可能大于N,此时消费者应该进行成批读取操作,将[R,N]范围 内的数据全部处理。


六、Disruptor 的相关组件

RingBuffer
       RingBuffer是存储消息的地方,通过一个名为cursor的Sequence对象指示队列的头,协调多个生产者向RingBuffer中添加消息,并用于在消费者端判断RingBuffer是否为空。巧妙的是,表示队列尾的Sequence并没有在RingBuffer中,而是由消费者维护。这样的好处是多个消费者处理消息的方式更加灵活,可以在一个RingBuffer上实现消息的单播,多播,流水线以及它们的组合。其缺点是在生产者端判断RingBuffer是否已满是需要跟踪更多的信息,为此,在RingBuffer中维护了一个名为gatingSequences的Sequence数组来跟踪相关Seqence。

Sequence
       Sequence是Disruptor最核心的组件。生产者对RingBuffer的互斥访问,生产者与消费者之间的协调以及消费者之间的协调,都是通过Sequence实现。几乎每一个重要的组件都包含Sequence。那么Sequence是什么呢?首先Sequence是一个递增的序号,说白了就是计数器;其次,由于需要在线程间共享,所以Sequence是引用传递,并且是线程安全的;再次,Sequence支持CAS操作;最后,为了提高效率,Sequence通过padding来避免伪共享。

SequenceBarrier
       SequenceBarrier用来在消费者之间以及消费者和RingBuffer之间建立依赖关系。在Disruptor中,依赖关系实际上指的是Sequence的大小关系,消费者A依赖于消费者B指的是消费者A的Sequence一定要小于等于消费者B的Sequence,这种大小关系决定了处理某个消息的先后顺序。因为所有消费者都依赖于RingBuffer,所以消费者的Sequence一定小于等于RingBuffer中名为 cursor的Sequence,即消息一定是先被生产者放到Ringbuffer中,然后才能被消费者处理。
       SequenceBarrier在初始化的时候会收集需要依赖的组件的Sequence,RingBuffer的cursor会被自动的加入其中。 需要依赖其他消费者和/或RingBuffer的消费者在消费下一个消息时,会先等待在SequenceBarrier上,直到所有被依赖的消费者和RingBuffer的Sequence大于等于这个消费者的Sequence。当被依赖的消费者或RingBuffer的Sequence有变化时,会通知SequenceBarrier唤醒等待在它上面的消费者。

Wait Strategy
       当消费者等待在SequenceBarrier上时,有许多可选的等待策略,不同的等待策略在延迟和CPU资源的占用上有所不同,可以视应用场景选择:
       BusySpinWaitStrategy :自旋等待,类似Linux Kernel使用的自旋锁。低延迟但同时对CPU资源的占用也多。
       BlockingWaitStrategy :使用锁和条件变量。CPU资源的占用少,延迟大。
       SleepingWaitStrategy :在多次循环尝试不成功后,选择让出CPU,等待下次调度,多次调度后仍不成功,尝试前睡眠一个纳秒级别的时间再尝试。这种策略平衡了延迟和CPU资源占用,但延迟不均匀。
       YieldingWaitStrategy :在多次循环尝试不成功后,选择让出CPU,等待下次调度。平衡了延迟和CPU资源占用,但延迟也比较均匀。
       PhasedBackoffWaitStrategy :上面多种策略的综合,CPU资源的占用少,延迟大。

EventHandler
       Disruptor定义的事件处理接口,由用户实现,用于处理事件,是 Consumer 的真正实现。

EventProcessor
       EventProcessor 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop)。

BatchEvenProcessor
       在Disruptor中,消费者是以EventProcessor的形式存在的。其中一类消费者是BatchEvenProcessor。每个 BatchEvenProcessor有一个Sequence,来记录自己消费RingBuffer中消息的情况。所以,一个消息必然会被每一个BatchEvenProcessor消费。

WorkProcessor
       另一类消费者是WorkProcessor。每个WorkProcessor也有一个Sequence,多个WorkProcessor还共享一个 Sequence用于互斥的访问RingBuffer。一个消息被一个WorkProcessor消费,就不会被共享一个Sequence的其他 WorkProcessor消费。这个被WorkProcessor共享的Sequence相当于尾指针。

WorkerPool
       共享同一个Sequence的WorkProcessor可由一个WorkerPool管理,这时,共享的Sequence也由WorkerPool创建。

Event
       在Disrupto 的语义中,生产者和消费者之间进行交换的数据被称为事件(Event)。它不是一个被Disruptor定义的特定类型,而是由Disruptor的使用者定义并指定。








0 0