告别Disruptor(一) 简洁优雅的高性能并发队列

作者|海带

编辑|包包    

几年前听说过Disruptor,一直没用过也没深究, 其号称是一个性能爆表的并发队列,上Github/LMAX-Exchange/disruptor 去看了看,官方性能描述文章 选了慢如蜗牛的ArrayBlockQueue来对比。在Nehalem 2.8Ghz – Windows 7 SP1 64-bit录得性能见后(其中P,C分别代表 Producer和Consumer):

1P – 1C 的吞吐量两千五百万次,1P – 3C Multicast 就降到了一千万次不到,对比我所认为的非线程安全1P -1C队列亿次每秒的量级,感觉并不强大。亿次每秒的队列加上线程安全,毛估估1P-1C性能减半五千万次每秒,1P-3C 再减少个30%三千五百万次每秒,应该差不多了吧。

继续读读Disruptor的介绍,整体业务框架先不谈,关于队列的部分,我只想问可以说脏话不,太TMD的复杂了,实现方式一点都不优雅,讲真,我想用100行代码灭了它。

说干就干,先尝试一下。

ArrayBlockingQueue Disruptor
Unicast: 1P – 1C 5,339,256 25,998,336
Pipeline: 1P – 3C 2,128,918 16,806,157
Sequencer: 3P – 1C 5,539,531 13,403,268
Multicast: 1P – 3C 1,077,384 9,377,871
Diamond: 1P – 3C 2,113,941 16,143,613

Comparative throughput (in ops per sec)

第一步 简单粗暴的阻塞队列 

先简单来个,这年头用synchronized要被人瞧不起的(风闻现在性能好多了),大家言必谈ReentrantLock,CAS,那么我也赶潮流CAS吧,主流程如下。

步骤 主要工作 失败流程
1 无论是put还是take,先对head/tail getAndIncrease 在Array中占位 100%成功
2 检查是否能够放/取 失败则循环检查,条件允许了再操作(不能返回失败,第1步的占位无法回退)
3 执行放/取操作

第2步的操作,如果失败,并非完全不能回退,只是需要牵扯到一套复杂的逻辑,在这个简单粗暴的实现中,先不考虑非阻塞方案。

核心代码如下:

private Object[] array;
private final int capacity;
private final int m;


private final AtomicLong tail;
private final AtomicLong head;
private final AtomicLong[] als = new AtomicLong[11];


public RiskBlockingQueue(int preferCapacity) {
  this.capacity = getPow2Value(preferCapacity, MIN_CAPACITY, MAX_CAPACITY);
  //这是个取比preferCapacity大的,最接近2的整数幂的函数(限制必须在 MIN MAX 之间)
  array = new Object[this.capacity];
  this.m = this.capacity - 1;    


  for (int i = 0; i < als.length; i++) {     // 并不一定100%成功的伪共享padding
    als[i] = new AtomicLong(0);
  }
  head = als[3];
  tail = als[7];
}


public void put(T obj) {
  ProgramError.when(obj == null, "Can't add null into this queue");
  int p = (int) (head.getAndIncrement() & this.m);
  int packTime = MIN_PACKTIME_NS;
  while(array[p] != null) {
    LockSupport.parkNanos(packTime);
    if(packTime < MAX_PACKTIME_NS) packTime <<= 1;
  }
  array[p] = obj;
}


public T take(){
  Object r;
  int p = (int) (tail.getAndIncrement() & this.m);
  int packTime = MIN_PACKTIME_NS;
  while((r = array[p]) == null) {
    LockSupport.parkNanos(packTime);
    if(packTime < MAX_PACKTIME_NS) packTime <<= 1;
  }
  array[p] = null;
  return (T)r;
}

代码简单的不要不要的,30来行代码,一个线程安全的阻塞就基本完成。什么?你问构造函数为什么叫RiskBlockingQueue?,很简单,有Risk,这并不是一个真正意义上的线程安全Queue,它有风险,那么风险在哪里呢?

各位看官先自己想想风险在哪里,我先来测个1P-1C 性能数据 (以下数据都在关闭了CPU超线程的环境下测试获得,超线程时数据经常看上去很美)

1P – 1C

Producer 0 has completed. Cost Per Put 19ns.
Consumer 0 has completed. Cost Per Take 19ns.
Total 201M I/O, cost 3844ms, 52374243/s(52M/s)

????,还真的和预测差不多,性能减半。

接下来,揭晓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值