2021SC@SDUSC BRPC代码分析(十一) —— bthread-WorkStealingQueue类详解

2021SC@SDUSC


一、简介

        上篇文章比较细致地介绍了bthread的整个调度流程。注意到里面在调度时出现了steal_task这种操作,偷任务这种说法挺起来很新鲜,它的内部实现我们上篇文章也见到了。其实是调用了TaskGroup的_rq,让_rq的成员函数去实现。
在这里插入图片描述
在这里插入图片描述

我们去task_group.h中找到TaskGroup类中定义了这个WorkStealingQueue _rq。所以这篇文章让我们去看一看WorkStealingQueue的代码实现。
在这里插入图片描述
首先先简单介绍一下这个WorkStealing的思想。这并不是brpc自创的思想,在很多多线程的地方都被使用到,比如JDK1.7引入的Fork/Join框架就是基于工作窃取算法。
work-stealing,工作窃取算法是指某个线程从其他队列里窃取任务来执行。
一个大任务分割为若干个互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并未每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如线程1负责处理1队列里的任务,2线程负责2队列的。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务待处理。干完活的线程与其等着,不如帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们可能会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务线程永远从双端队列的尾部拿任务执行。工作窃取算法充分利用线程进行并行计算,减少线程间的竞争。
在这里插入图片描述

二、代码分析

首先看一下工作窃取队列的属性,因为是双向访问的队列,所以有_bottom和_top头尾两个属性,还有容量_capacity,_buffer就是实际保存数据的数组。
在这里插入图片描述
然后来看函数。首先是构造和析构函数。未初始化前容量为0,_bottom和_top相等,均为1,buffer未分配空间。析构时直接删除buffer即可。
在这里插入图片描述
然后是init初始化函数,注意判断了传入参数capacity即数组容量必须是2的幂,这个要求和上面bottom和top初始化为1都是为了快速定位到访问元素并且支持循环访问(如果容量是2的幂,就可以直接用按位与了,提高效率)。在这里插入图片描述
它本质上就是一个双向队列的实现,所以肯定会有任何一个队列都有的push()和pop()操作。
push()的原则是往队列的bottom侧添加元素,只有worker自己会调用自己的push操作,对应到使用中它和pop()或者另一个push()不会同时运行,但有可能和steal()同时运行,成功返回true,因为满了无法push返回false(bottom和top都可以一直增加,所以容量判断采取插值的比较)。
然后去判断位置写入数据,选择位置的方法是b&(_capacity-1),这怎么理解呢?因为_capacity被设定为2的幂,二进制形式就是0010、0100、1000这样某一位为1其他为0,分别对应的(_capacity-1)是0001、0011、0111,利用一直加和按位与可以巧妙地实现队列地循环,是效率更高的一种方法。
注意bottom的store用了release,这是为了保证对_buffer的数据写入对steal的acquire读取可见(memory_order相关的代码分析可能会在后续提到)。在这里插入图片描述
然后看pop()函数,pop()是从bottom侧取数据,因为会和从top侧取的steal()同时运行,所以这个并行问题是必须要考虑进去的,解决的思想是先把bottom减1,相当于提前锁定这个对应元素,防止被steal()取,从注释看到在只有一个元素的时候会和steal竞争。t>newb判断是否有可取元素,如果没有就直接返回false。有的话暂存准备返回的数据,t如果等于newb那么说明是最后一个元素,有可能其他的steal也在取这个元素,因此随后用compare_exchange_strong去竞争,如果成功则返回,否则回退掉b的改动。
在这里插入图片描述
steal()是要从_top侧读数据,因此需要butil::memory_order_acquire读bottom确保push写入的buffer元素的可见性。
t>=b认为是空队列直接返回失败,这里说的false negtive是指t>=b的时候队列可能并不空,因为_bottom和_top这种共享变量都是有同步延迟的。
然后去执行具体的steal_task逻辑,先预读后尝试为t加1,因为在执行过程中会和其他的steal、pop竞争,因此可能会失败,所以用了一个do-while循环,只要队列不为空就持续尝试steal。
在这里插入图片描述
获取队列中现在的大小,我们注意到它叫volatile_size不稳定、易改变的大小,因为上文提到_bottom和_top这种共享变量都是有同步延迟的,所以这种值只能供参考,并不精准。
在这里插入图片描述

总结

以上就是今天介绍的全部内容,本文进行了bthread调度的工作窃取算法WorkStealingQueue部分详细源码分析,之后的博客会继续进行其他代码的分析。

猜你喜欢

转载自blog.csdn.net/m0_46306466/article/details/121939200