bthread是协程吗?
如果你使用过brpc,那么对bthread应该并不陌生。毫不夸张地说,brpc的精华全在bthread上了。bthread可以理解为“协程”,尽管官方文档的FAQ中,并不称之为协程(见:apache/incubator-brpc)。
若说到pthread大家都不陌生,是POSIX标准中定义的一套线程模型。应用于Unix Like系统,在Linux上pthread API的具体实现是NPTL库实现的。在Linux系统上,其实没有真正的线程,其采用的是LWP(轻量级进程)实现的线程。而bthread是brpc实现的一套“协程”,当然这并不是传统意义上的协程。就像1个进程可以开辟N个线程一样。传统意义上的协程是一个线程中开辟多个协程,也就是通常意义的N:1协程。比如微信开源的libco就是N:1的,libco属于非对称协程,区分caller和callee,而bthread是M:N的“协程”,每个bthread之间的平等的,所谓的M:N是指协程可以在线程间迁移。熟悉Go语言的朋友,应该对goroutine并不陌生,它也是M:N的。
当然准确的说法goroutine也并不等同于协程。不过由于通常也称goroutine为协程,从此种理解上来讲,bthread也可算是协程,只是不是传统意义上的协程!当然,咬文嚼字,没必要。
要实现M:N其中关键就是:工作窃取(Work Stealing)算法。不过在真正展开介绍工作窃取之前,我们先透视一下bthread的组成部分。
bthread的三个T
讲到bthread,首先要讲的三大件:TaskControl、TaskGroup、TaskMeta。以下简称TC、TG、TM。
TaskControl进程内全局唯一。TaskGroup和线程数相当,每个线程(pthread)都有一个TaskGroup,brpc中也将TaskGroup称之为 worker。而TM基本就是表征bthread上下文的真实结构体了。
虽然前面我们说bthread并不严格从属于一个pthread,但是bthread在运行的时候还是需要在一个pthread中的worker中(也即TG)被调用执行的。
从bthread_start_background讲到TM
以下开启Hard模式,我们先从最常见的bthread_start_background来导入。bthread_start_background是我们经常使用的创建bthread任务的函数,源码如下:
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);
}
函数接口十分类似于pthread 的pthread_create()。也就是设置bthread的回调函数及其参数。
如果能获取到thread local的TG(tls_task_group),那么直接用这个tg来运行任务:start_background()。
看下start_background源码,代码不少,但可以一眼略过。
template <bool REMOTE>
int TaskGroup::start_background(bthread_t* __restrict th,
const bthread_attr_t* __restrict attr,
void * (*fn)(void*),
void* __restrict arg) {
if (__builtin_expect(!fn, 0)) {
return EINVAL;
}
const int64_t start_ns = butil::cpuwide_time_ns();
const bthread_attr_t using_attr = (attr ? *attr : BTHREAD_ATTR_NORMAL);
butil::ResourceId<TaskMeta> slot;
TaskMeta* m = butil::get_resource(&slot);
if (__builtin_expect(!m, 0)) {
return ENOMEM;
}
CHECK(m->current_waiter.load(butil::memory_order_relaxed) == NULL);
m->stop = false;
m->interrupted = false;
m->about_to_quit = false;
m->fn = fn;
m->arg = arg;
CHECK(m->stack == NULL);
m->attr = using_attr;
m->local_storage = LOCAL_STORAGE_INIT;
m->cpuwide_start_ns = start_ns;
m->stat = EMPTY_STAT;
m->tid = make_tid(*m->version_butex, slot);
*th = m->tid;
if (using_attr.flags & BTHREAD_LOG_START_AND_FINISH) {
LOG(INFO) << "Started bthread " << m->tid;
}
_control->_nbthreads << 1;
if (REMOTE) {
ready_to_run_remote(m->tid, (using_attr.flags & BTHREAD_NOSIGNAL));
} else {
ready_to_run(m->tid, (using_attr.flags & BTHREAD_NOSIGNAL));
}
return 0;
}
主要就是从资源池取出一个TM对象m,然后对他进行初始化,将回调函数fn赋进去,将fn的参数arg赋进去等等。
另外就是调用make_tid,计算出了一个tid,这个tid会作为出参返回,也会被记录到TM对象中。tid可以理解为一个bthread任务的任务ID号。类型是bthread_t,其实bthread_t只是一个uint64_t的整型,make_tid的计算生成逻辑如下:
inline bthread_t make_tid(uint32_t version, butil::ResourceId<TaskMeta> slot) {
return (((bthread_t)version) << 32) | (bthread_t)slot.value;
}
version是即TM的成员version_butex(uint32_t*)解引用获得的一个版本号。在TM构造函数内被初始化为1,且brpc保证其用不为0。在TG的相关逻辑中会修改这个版本号。
TaskControl
先看TC是如何被创建的,TC对象的直接创建(new和初始化)是在get_or_new_task_control()中,这个函数顾名思义,就是获取TC,有则返之,无则造之。所以TC是进程内全局唯一的,也就是只有一个实例。而TG和TM是一个比一个多。
下面展示一下get_or_new_task_control的被调用链(表示函数的被调用关系),也就能更直观的发现TC是如何被创建的。
- get_or_new_task_control
- start_from_non_worker
- bthread_start_background
- bthread_start_urgent
- bthread_timer_add
- start_from_non_worker
我们通常用的bthread_start_background()或者定期器bthread_timer_add()都会调用到get_or_new_task_control。
回顾一下bthread_start_background的定义:
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

本文深入剖析了brpc中的bthread,虽然bthread不严格等同于协程,但它实现了M:N的协程模型,依赖工作窃取算法实现线程间的任务迁移。文章详细介绍了bthread的组成部分,如TaskControl、TaskGroup和TaskMeta,以及工作窃取的关键过程,包括wait_task、steal_task和task_runner等函数。通过对bthread_start_background等函数的分析,展示了bthread如何创建、调度和执行任务,揭示了brpc中协程调度的内部机制。
最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



