二叉树的几种基本操作的机制_消息队列优化 -- 几种基本实现

[我的blog原地址:消息队列优化 -- 几种基本实现,后续会持续努力同步更新到知乎~ ]


听说今天不发blog就要等四年了,于是占个坑。

1. 基本实现与对比

这两周写了一部分很简单的实现,陆续上传到:

https://github.com/holmes1412/queue

随着进一步的了解,现在可以分别列一下我们这次关注的几种实现的对比:

936c55636846b61eeda435716d103b5a.png


另外说明:kfifo和work_stealing_queue是无阻塞的, 并且因为kfifo和work_stealing_queue的基本操作都是满足单生产者单消费者,所以这里对比的时候还是按照用他们去实现多生产者多消费者来跟其他的实现相比。

其中前面三种实现(外加最笨的单锁队列锁内唤醒版本)也用来切了这道题:

https://leetcode-cn.com/problems/design-bounded-blocking-queue/

d6a1dbee938d37d097d2c92f48c53ec6.png

从最早到最近分别是这几版队列,不过leetcode的多线程题的耗时是非常说明不了问题的,所以自己写了个demo,但是这个测试场景怎么才最能说明问题是比较需要想想的,而且由于workstealing要扩展到多生产者多消费者还需要封装消费者线程池,所以剩下的工作都打算放到下一篇说了。

2. CAS

这里选取linux内核的kfifo.h来了解下,内核代码写得非常漂亮。kfifo是一个使用循环数组来实现的first-in-first-out队列的,在单生产者单消费者的情况下,可以做到彻底无锁。如果多生产者多消费者,可以使用带spinlock的接口。先看下单生产者单消费者无锁的原理:

struct 

几个说明:

  1. 队列在初始化时会记录下maskcapacity - 1,这是用于循环数组寻址把 pos % capcity 转成 pos & (capacity - 1)
  2. 为了循环队列能够快速寻址,pos使用了上述转换,因此capacity必须是2的幂
  3. in是入队列的位置,out是出队列的位置,两个值会一直加到unsigned int溢出回到0都是正确的;
  4. out()接口可以看到带了个len参数,这是一批我要放的元素的个数,内部实现是先算一下当前是不是够放、应该往哪放,然后先算出往数组后半部分可以放的个数,然后要是没放完就从数组[0]位置继续放;get()也是类似的道理;
  5. 必须先操作完再更新in或out的值;
  6. 使用了smp_wmb()做内存屏障,保证对方正确观察到我的执行顺序。

关于带spinlock的接口,因为自旋锁是busy-waiting,比较适合锁上之后很快释放的场景,这样避免互斥锁切线程的性能损耗,适合争抢的人数小于等于cpu数的场景,否则会导致cpu cache频繁失效性能会急剧下降。所以在实际使用带spinlock的模块时,线程数怎么根据使用情况和模块本身的特点来配就很重要了。

而我们作为一个通用的通信队列,生产者/消费者线程数量肯定远远大于cpu数(因为通信场景除了对epoll进行操作以外,还会做一些read/write,甚至是序列化反序列化的事情,而我们作为框架提供者,不能保证任何一个服务哪部分资源使用会更多),如果大家都在比较闲,就都忙等spinlock,这显然非常不合理。

因此我个人认为,CAS这样看来是不太适合一个通用的队列实现的,但是这两天又看到许多关于lock-free的资料,还得深入了解下。如果大家有不一样的场景也欢迎交流~

3. work stealing

前面几种具体实现可以说都属于work sharing(所有线程共享一个队列),而这里是线程不够消费了再去steal。所以这里上升到一个更大的概念的话,里边具体的优化点就有很多,比如:

  • 怎样把任务分配给每个线程
  • 怎么steal
  • 另外,work stealing更多的是用于线程调度,所以业内有些优化是针对于减少提交时的线程切换做的,这种优化我们做队列时用不上。

想了想发现不太好写,于是看了下brpc内部的源码,它就满足了谢爷经常说的机制要和策略分离的架构设计理念。它实现的机制是:先从一个单生产者单消费者队列开始封装,如果使用者想用到多生产者多消费者场景,可以对每个消费者做一个队列,然后调用别人的队列的steal( )接口,使用者去实现steal具体策略。最基本的实现竟然和kfifo.h有非常相似的地方:

https://github.com/apache/incubator-brpc/blob/master/src/bthread/work_stealing_queue.h

  1. 非阻塞
  2. 队列capacity都会初始化成2^n,都是为了队列内循环数组寻址方便;
  3. 都是用了内存屏障去保证自己修改的pos能被看到;

steal( )接口值得看看,我按自己的理解加了点注释:

// Steal one item from the queue.

扩展到多生产者多消费者的场景,workstealing的代价应该就比较明显了:

  • 长尾肯定比work sharing的任何做法都明显;
  • 需要的额外操作更多了,如果steal的策略是查看各队列个数,那么每个队列都要额外维护一个原子变量;如果想要针对各个线程的当前吞吐进行任务分配,更要持续检查每个队列的负载情况等。
  • 全局的顺序性没法保证(所以不能用来切leetcode 1188了,它要求dequeue的顺序必须严格按照enqueue来)。很多使用本身不需要有序,但是如果整体顺序做得比较好,那么外部的并行提交就能比较快的被同时处理完成,是个很友善的功能。

4. 其他

size

leetcode这道题要求实现size( )接口,然而很多队列实现size是不太确定的,work_stealing_queue的size接口叫做volitale_size(),也是因为这类队列的size本身就是不准确的。

平均等待个数

队列中平均等待个数应该是一个很重要的提示,不同的使用场景导致内部等着的节点数量不同,所能用的优化方式就不一样。

超时

队列的接口是否支持超时也是很重要的设计,内部实现越简单,超时就越好做。

内存屏障的使用

各种内存屏障的用法不一,消耗也是不一样的,完全值得以后单独写一篇来学习下。

最后

为了平衡吞吐和长尾,不可能对所有场景有完美的解决方案,但是不同场景的几种优化(比如linux内核的kfifo, google的Linked_Blocking_Queue,还有我们当前的队列交换)其实都已经做得非常漂亮了,经典的代码还是值得学习和动手写一下的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值