bthread了解
转自:https://zhuanlan.zhihu.com/p/294129746 ,讲的很深奥,目前还看不懂。
https://github.com/apache/incubator-brpc/blob/master/docs/cn/bthread.md
https://ehds.github.io/2021/07/20/bthread_schedule/
1.介绍
bthread是brpc实现的一套“协程”,bthread是M:N的“协程”,每个bthread之间的平等的,所谓的M:N是指协程可以在线程间迁移。
M是指有M个协程,N个线程。”M:N“是指M个bthread会映射至N个pthread,一般M远大于N。
要实现M:N其中关键就是:工作窃取(Work Stealing)算法。
三大件:
- TaskControl:进程内全局唯一。
- TaskGroup:和线程数相当,每个线程(pthread)都有一个TaskGroup,也称为worker,协调 bthread 的调度。
- TaskMeta:表征bthread上下文的真实结构体。
bthread并不严格从属于一个pthread,但是bthread在运行的时候还是需要在一个pthread中的worker中(也即TG)被调用执行的。
2.bthread与协程
协程特指N:1线程库,即所有的协程运行于一个系统线程中,由于不跨线程,协程之间的切换不需要系统调用,可以非常快(100ns-200ns)。但代价是协程无法高效地利用多核,代码必须非阻塞,否则所有的协程都被卡住。(意思是说,因为所有协程都是属于一个线程的,并且一个线程下只能有一个协程运行,那如果这个正在运行的协程阻塞了,其他的协程也没办法切换?)
bthread是一个M:N线程库,一个bthread被卡住不会影响其他bthread,可以更好的利用多核。关键技术两点:
- work stealing调度:让bthread更快地被调度到更多的核心上;
- butex:让bthread和pthread可以相互等待和唤醒。(?什么意思?)
3.pthread运行bthread
pthread worker在任何时间只会运行一个bthread,当前bthread挂起时,
- pthread worker先尝试从本地runqueue弹出一个待运行的bthread,
- 若没有,则随机偷另一个worker的待运行bthread,
- 仍然没有才睡眠并会在有新的待运行bthread时被唤醒。
【上述的也就是work stealing算法吧】
4.bthread与阻塞
一个bthread阻塞会影响其他bthread吗?
不影响。
- 若bthread因bthread API而阻塞,它会把当前pthread worker让给其他bthread。
- 若bthread因pthread API或系统函数而阻塞,当前pthread worker上待运行的bthread会被其他空闲的pthread worker偷过去运行。
5. TaskGroup
void TaskGroup::run_main_task() {//入口 ...... TaskGroup* dummy = this; bthread_t tid; while (wait_task(&tid)) {//不断的 wait_task, 等到 bthread 后 TaskGroup::sched_to(&dummy, tid);//即进入 sched_to 开始调度该 bthread DCHECK_EQ(this, dummy); DCHECK_EQ(_cur_meta->stack, _main_stack); if (_cur_meta->tid != _main_tid) { TaskGroup::task_runner(1/*skip remained*/); } ...... }
在 TaskGroup 中有两个用于存储 bthread 的队列:
WorkStealingQueue<bthread_t> _rq;
RemoteTaskQueue _remote_rq;
- WorkStealingQueue 用于存储由 task_group(worker) 自身生成的 bthread,
- RemoteTaskQueue 用于存储非 task_group 生成的bthread
WorkStealingQueue 中的 push 和 pop 只会被本 worker 调用,仅与 steal 产生竞争(意思是可能有其他pthread来竞争这个队列的push和pop?),所以使用无锁实现减少锁的开销。RemoteTaskQueue 由于是由非 worker 调用,且是随机选择task_group, 所以使用锁的方式实现(这个队列存储的意思是如果他的workstealingqueue是空的话,它就去经侦这个remote队列的bthread去执行?)。
流程是:
如果本 taskgroup 的 _rq 没有任务时,就会触发 steal_task。task_group 的 steal_task 会首先从本地的 _remote_rq 取任务,如果没有则会从其他 task_group 拿任务。通过 task_control 这个管理者实现从其他 task_group 拿任务:
bool stolen = false; size_t s = *seed;//从随机位置开始遍历 for (size_t i = 0; i < ngroup; ++i, s += offset) {//每次跨度 offset,遍历 ngroup 次 TaskGroup* g = _groups[s % ngroup]; // g is possibly NULL because of concurrent _destroy_group if (g) { if (g->_rq.steal(tid)) { stolen = true; break; } if (g->_remote_rq.pop(tid)) { stolen = true; break; } } }
先从其他 task_group 的 working_stealing_queue 里面取,其次从remote_rq 取;取到任务后立即返回。
6.bthread_start_background开启bthread
bthread_start_background 相当于pthread_create,
int bthread_start_background(bthread_t* __restrict tid, const bthread_attr_t* __restrict attr, void * (*fn)(void*), void* __restrict arg) { bthread::TaskGroup* g = bthread::tls_task_group; if (g) { // start from worker return g->start_background<false>(tid, attr, fn, arg); } return bthread::start_from_non_worker(tid, attr, fn, arg); }
如果创建这个bthread的调用者是外部的线程(非task_group)【??外部的线程怎么理解??】,调用start_from_non_worker。start_from_non_worker的工作就是从task_control随机选择一个task_group作为这个新bthread的归宿。 bthread被加入 remote_rq。
如果创建者是已经存在于task_group的bthread则会以当前的task_group作为最终归宿。task_group中的start_background做的工作就是为新的bthread创建资源TaskMeta,然后将它加入remote_rq/_rq。
7.bthread与brpc
利用epoll监听读写事件,然后分发处理,事件监听调度EventDispatcher运行在bthread中,在brpc中读事件一般分为两种:
1.新的请求连接(OnNewConnections)2.读socket上的message (OnNewMessages)
如果是发生读事件,会抢占当前 epoll 所在 task_group 的执行权限,会调用bthread_start_ugrent来执行读事件;那么正在执行的bthread会被挂起到 _rq 队列中(可以被其他 task_group steal_task ),读事件 handler 的 btrhead 会被调度到当前task_group中(发生上下文切换 )。