2021SC@SDUSC BRPC代码分析(十) —— bthread-parking_lot调度类详解

2021SC@SDUSC


一、简介

        上篇文章简单介绍了bthread的整个流程和使用,其中我提到了bthread的具体调度问题,在brpc里,有个和调度相关的重要类parking_lot类,这篇文章我将重点介绍bthread在各个bthread_worker(TaskGroup)之间的调度方式。
在这里插入图片描述
parking_lot根据代码可以看出本质上就是基于futex的实现的wait、signal。
简单介绍一下futex。有机会会分析sys_futex.h中的各种实现。
futex (fast userspace mutex) 是Linux的一个基础组件,可以用来构建各种更高级别的同步机制,比如锁或者信号量等等,POSIX信号量就是基于futex构建的。大多数时候编写应用程序并不需要直接使用futex,一般用基于它所实现的系统库就够了。
futex的解决思路是:在无竞争的情况下操作完全在user space进行,不需要系统调用,仅在发生竞争的时候进入内核去完成相应的处理(wait 或者 wake up)。所以说,futex是一种user mode和kernel mode混合的同步机制,需要两种模式合作才能完成,futex变量必须位于user space,而不是内核对象,futex的代码也分为user mode和kernel mode两部分,无竞争的情况下在user mode,发生竞争时则通过sys_futex系统调用进入kernel mode进行处理。

二、代码分析

parking_lot中文翻译为停车场,表示有bthread_worker会停在这里,形容很形象准确。
来看ParkingLot类代码。这个类的代码量不多,属性只有一个,_pending_signal这个就是用于wait和signal的futex变量,需要注意正如注释所说,留下最低位作为一个是否停止的标识。
在这里插入图片描述
有四个函数,基本都是在调用futex的方法实现它对于bthread_worker的调度。
signal()是唤醒num_task个等待在_pending_signal上的线程,内部实现就是调用futex_wake_private,在调用之前对_pending_signal执行了原子加,加的是num_task << 1,之所以要左移是因为第一位是用于表明是否停止的标识位。
wait()是如果_pending_signal的当前值和先前拿到的expected_state.val相等的话就wait。内部调用的是futex_wait_private。
在这里插入图片描述
stop()可以看出是对_pending_signal最末位与1做或操作,相当于将最后一位用于表示是否停止的标识置1。然后唤醒所有wait的线程。可以在State类中看到有bool类型的stopped()函数判断最后一位是否为1。后面会再提到怎么被使用。
在这里插入图片描述
get_state是获取用于wait的状态,就是直接返回_pending_signal的值,返回类型是State,State是ParkingLot的内部类,因为有一个int参数的构造函数,所以可以自动转换,如下。
在这里插入图片描述

为了避免竞争太激烈,brpc会用将worker分配到多个pl上去,TaskGroup有一个ParkingLot* _pl变量,记录了本worker所用的pl,初始化TaskGroup的时候赋值如下,将pthread_numeric_id根据PARKING_LOT_NUM取余,
在这里插入图片描述

目前brpc设置PARKING_LOT_NUM = 4。
在这里插入图片描述
TaskGroup类里和bthread调度直接相关的两个主要函数为sched和sched_to,前者是阻塞当前TaskGroup按照调度规则从队列里调度下一个bthread,后者是让出当前TaskGroup根据tid直接调度指定TaskGroup,sched如下。
work_stealing_queue(WSQ)的push和pop都是在bottom一侧,而steal是在top一侧,如果没有BTHREAD_FAIR_WSQ宏定义,会使用pop,否则是用steal,从FIFO的角度来说,使用steal是更公平的(所以宏定义称之为FAIR),但是开销会更大。
在这里插入图片描述
下面就从一个bthread_woker的创建开始整体看一下bthread的调度,前面关于bthread流程的文章提到过,在TaskControl的init里,会启动worker,worker入口函数是run_main_task(),核心代码如下。我们在之前一篇文章介绍过,这篇文章从调度的角度在回顾分析一下。

在这里插入图片描述
Wait_task函数就用到了上面提到的_pl,这是futex机制的典型使用方式。
首先整个函数内容是一个死循环,也就是我们常说的自旋锁,因为futex机制的特性,通常都是结合自旋锁来使用。执行里面会根据BTHREAD_DONT_SAVE_PARKING_STATE宏定义是否存在来确定执行的代码,如果没有定义BTHREAD_DONT_SAVE_PARKING_STATE,说明要保存_pl的状态,则会根据上一次的steal_task里保存的状态来判断。
在这里插入图片描述
可以在TaskGroup.h中看到如果没有这个宏定义会在steal_task()时获取状态。
在这里插入图片描述
总的来说,就是不断循环进行以下操作:
判断_pl是否处于停止状态,如果是,则直接返回-1,pl被调用stop()后会进入停止状态,正常运行过程中stop不会被调用。
尝试steal_task,如果取到了task则返回true,没取到则根据上次的状态进行wait,因为是在循环里,根据futex的机制,如果上一个state和当前_pl上的state一致,那么说明_pl上的任务没变化,继续steal没有意义,则wait,否则说明有其他地方调用_pl上的signal,也就是有新的任务加到某个队列里,_pending_signal也会发生变化,steal有可能成功。如果进入了wait,在_pl的signal被调用的时候也会被唤醒。
steal_task()实际是在调用TaskControl中的steal_task函数,代码如下。
依次按顺序从各个bthread_woker里取,优先选择本地队列,然后选取远程队列。
以上就是bthread_woker从创建到运行的机制,也就是一直循环取task然后执行,有可能wait在某个ParkingLot上等待唤醒。
在这里插入图片描述
还有一种调度的情况是在bthread执行过程中调用bthread_yield让出当前的worker,代码如下。
在这里插入图片描述
实际上去调用了bthread::TaskGroup::yield(&g);。首先是用set_ramained把 ready_to_run_in_worker和当前bthread注册到下个bthread启动前的回调里,然后使用上面提到过的sched调度下一个TaskGroup,这样在下个bthread执行前会将让出worker的该bthread放到队列里供调度执行。
在这里插入图片描述

总结

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

猜你喜欢

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