打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
disruptor框架为什么这么强大

    disruptor是LMAX的一个并发框架,在很难再继续压榨CPU的今天,disruptor显然又挑战了极限。LMAX可以达到单线程每秒6百万订单,用1微秒的延迟获得吞吐量为100K+。真是让人惊叹不已。

    那么,disruptor到底为什么这么强大呢,有很多文章都对其进行了描述(见参考)。

    归结下来有这么几点:

    1. 弃用锁机制转而使用CAS

        Disruptor论文中讲述了我们所做的一个实验。这个测试程序调用了一个函数,该函数会对一个64位的计数器循环自增5亿次。当单线程无锁时,程序耗时300ms。如果增加一个锁(仍是单线程、没有竞争、仅仅增加锁),程序需要耗时10000ms,慢了两个数量级。更令人吃惊的是,如果增加一个线程(简单从逻辑上想,应该比单线程加锁快一倍),耗时224000ms。

        所以锁的开销实际上已经被证实是非常的大了,如果减少锁的使用,降低锁的粒度,这是disruptor提供给我们的另外一种思路。

    2. 为了解决伪共享而引入缓存行填充

        伪共享讲的是多个CPU时的123级缓存的问题,通常,缓存是以缓存行的方式读取数据,如图,当两个CPU同样都把X,Y load到自己的一级缓存时,实际上CPU为了争用X,Y还是会在写入时发生竞争,这样同样会导致锁,效率下降这些问题。


         如何解决这个问题呢?实际上使用的是与内存对齐一样的方法,就是每次把数据对齐到跟缓存行(通常是64B)一样大小。其实内存对齐和解决伪共享的缓存行对齐其实是同一种思路。所以在disruptor源码中可以看到这样一个内部类,它为什么写成这样,其实就是为了缓存行对齐。

  1. private static class Padding  
  2. {  
  3.     /** Set to -1 as sequence starting point */  
  4.     public long nextValue = Sequence.INITIAL_VALUE, cachedValue = Sequence.INITIAL_VALUE, p2, p3, p4, p5, p6, p7;  
  5. }  

    3. 使用一种独特的数据结构RingBuffer来代替Queue。这个数据结构的使用使得弃用锁转而用CAS成为了可能。

         大家可以想象,如果要自己编写一段生产者消费者的程序,你会怎么做呢?

         当然了,大多数人都会用一个队列来实现,说白了,就是把队列作为生产者和消费者之间的缓冲,从而间接的同步了他们的速度,使得异步编程成为可能。

         说到异步编程,可能大家都再熟悉不过了,实际上Java的图形界面就是一个典型的异步编程的例子,作为处理界面事件的消费者其实只有一个线程,而更新界面的生产者会是多个线程,由于采用了生产者消费者模型,生产者生产的界面事件会保存在一个队列里,由这个内置的线程统一去做更新,这样就保证了我们编程时不会人为破坏界面绘制的过程,从而提高代码质量,降低复杂度。顺带一提,.net的delegate也是同样的道理,如果用其他线程更新界面会出现很多怪异的问题,所以delegate通过一个事件队列,同样实现了消费者在一个单独的线程中。

         扯远了。总之我们会使用一个队列来处理这种问题,这里存在几个问题,第一,如果生产者生产过快,一定时间后,队列会变得过度膨胀,占用内存空间;第二,看过队列的实现会发现,为了保证多个线程访问的正确性,在操作队列时是一定要加锁的,前面也说了,加锁以后时间会慢几个量级。

         disruptor框架的设计带我们走出了这个思维定势。我们一定要用不断增长的队列吗?我们访问队列一定要加锁吗?答案都是否定的。

         所谓的ringbuffer,就是一个环形队列。我们来看看为了解决这两个问题他是如何做的。

         1. 一个环形队列,意味着首尾相连,也就是他的大小是有限制的,但是ringbuffer是基于这样一个假设:即生产者和消费者都在同步往前走,不存在某边特别快,这样这个对列就可以循环使用,不必有创建对象的开销,另外,ringbuffer由于是固定大小的,使用数组来保存,由于预先分配好了空间,所以定位和查找的速度自然不必说。所以它自然的解决了第一个问题。但是如果实际使用中确实是生产者快于消费者(或者反过来)呢?也就是说,在某个时间点,他们一定会首尾相遇,这时候会发生什么呢?这个问题我们随后解释。

         2.为了让队列安全的在多线程环境运行,需要整个队列上锁,带来的开销是巨大的。来看ringbuffer是如何无锁的(简化起见,讨论一个生产者一个消费者的情况)。为了解释这个原理,这里借用别人的图来阐述这个问题。比如目前有一个consumer,停留在位置12,这时producer假设在位置3,这时producer的下一步是如何处理的呢?producer会尝试读取4,发现没有到下一个consumer,所以可以安全获取,于是将之改为14,并且将产品publish到14,并且通知一个阻塞的consumer起来活动。如此一直到11都是安全的(这里我们假设生产者比较快),当producer尝试访问12时发现不能继续,于是交出控制权;而consumer开始移动时,会调用barrier的waitFor方法,waitFor看到前面最近的安全节点已经到了20(21是producer),于是直接返回20,所以现在consumer可以无锁的去消费13到20的所有产品,可以想象,这种方式比起synchronized要快上很多倍。



参考:disruptor架构http://martinfowler.com/articles/lmax.html

disruptor文章集合:http://coolshell.cn/articles/9169.html

disruptor性能测试:https://code.google.com/p/disruptor/wiki/PerformanceResults

伪共享:http://coderplay.iteye.com/blog/1486649

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
disruptor 介绍
Disruptor的并发优化
使用Disruptor的几个代码演示
构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue
Disruptor源码解析
一种高效无锁内存队列的实现 ? 搜索技术博客-淘宝
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服