brpc中的协程bthread源码剖析(一):Work Stealing以及任务的执行与切换

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

我们通常用的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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

果冻虾仁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值