1、WorkStealingQueue
WorkStealingQueue是一种无锁队列,用做bthread中的任务队列。WorkStealingQueue提供三种操作:push,pop,steal。由于push和pop都是在本线程中完成的,因此push和pop操作不会并发,两个push和两个pop之间也不会并发,但是steal是从其他线程发起的,因此会和push或pop并发。
源码位置:bthread/work_stealing_queue.h
基本操作
队列保存头尾指针_top和_bottom,push操作在_bottom侧执行,pop操作在_bottom侧执行,steal操作在_top侧执行,如图示
并发正确性分析
push vs steal
push中
1、set _bottom = _bottom + 1
steal中
2、see new _bottom
3、set _top = _top + 1
push和steal并发的正确性很容易推理,要么steal没看到_bottom的新值返回失败,要么steal看到了_bottom的新值,从_top侧拿到数据。_bottom使用acq-rel语义保证数据内容对steal的可见性
pop vs steal
pop中
1、set _bottom = _bottom - 1
2、see _top == _bottom
3、try to set _top = _top + 1
steal中
4、see _top == _bottom - 1
5、try to set _top = _top + 1
顺序:
4,1,2,3,5:pop成功拿到数据,steal在(5)失败
4,1,2,5,3:steal成功拿到数据,pop在(3)失败
其他情况均会有一个操作提前失败无法执行到最后一步,因此正确性得到保障。
2、RemoteTaskQueue
基于base::BoundedQueue实现的有容量限制的任务队列,用于非工作线程提交任务。在brpc中,每个工作线程维护一个RemoteTaskQueue,非工作线程没有自己的任务队列,因此非工作线程需要通过工作线程提交任务。由于非工作线程随机选择一个工作线程的RemoteTaskQueue提交任务,本身就降低了访问同一个RemoteTaskQueue的冲突,因此RemoteTaskQueue的实现比较简单,内置一把锁用于同步,支持push和pop两种操作。
源码地址:bthread/remote_task_queue.h
3、RingBuffer
RingBuffer是环型数组,用做bthread的全局调度队列,一般情况下wait-free
数据结构
RingBuffer是一段连续空间的用户类型T的数组,到达尾部后下个槽位返回头部,因此逻辑上是环型的,数组长度是2的幂。RingBuffer通过引入version的概念实现了wait-free,这里以数组长度为4举例
概念解释:
index:RingBuffer维护递增的index,从0开始,标记下一次进行操作的槽位,图中并未显示。push和pop操作维护各自的index,即一共有两个index,next_push_index和next_pop_index。当next_pop_index更大,说明有消费者需要等待,此时消费者在该槽位睡眠等待next_push_index追上后唤醒
物理槽位号:物理槽位就是数组下标,这里共4个槽位,槽位号从0~3。index >> _slot_bits得到槽位号,这里4个槽位_slot_bits就是2(1 << 2 == 4)
round:index & _slot_version(图中4个槽位就是0x03),图中所示为进行了4个round后的情况
version:每个round version会发生2次变化。上一个round的终止version是2 * round,本轮push操作完成后,version变为2 * round + 1,pop操作完成后,version变为2 * round + 2,也是下个round的起始version
wait-free条件
RingBuffer在一般情况下是wait-free的,换句话说RingBuffer的wait-free需要一定条件,对于push操作,如果当前队列未满,那么push是wait-free的;对于pop操作,如果当前队列有待消费的数据,那么pop操作是wait-free的。这里分开解释下wait-free的原因。
对于push操作,对_next_push_index执行fetch_add操作获取全局唯一index,于是这个push操作的(槽位,round)是唯一的。由于队列未满,此槽位的上一轮pop一定已经完成了,当前version == 2 * round,push操作执行完成,不会发生阻塞,因此是wait-free的。
对于pop操作,对_next_pop_index执行fetch_add获取全局唯一index,这个pop操作的(槽位,round)是唯一的。由于队列中有待消费数据,因此上一轮push一定已经完成了,当前version == 2 *round + 1,pop操作执行完成,不会发生阻塞,因此是wait-free的。
对于队列满的情况,说明消费能力不足,此时push操作只能spin等待别无他法;对于队列空的情况,消费者在自己获取的槽位上睡眠等待唤醒。
4、ParkingLot
ParkingLot即睡眠点,当bthread的工作线程偷完一遍发现没有任务可偷时,会在睡眠点进入睡眠等待唤醒。重启点数量是在大量无效唤醒和无法及时唤醒之间做的权衡,当前bthread共有4个重启点,每次需要唤醒时将一个唤醒点的线程唤醒。ParkingLot实现较简单,每个睡眠点在一个futex上等待。源码位置:bthread/parking_log.h