bthread 是 brpc 使用的 M:N 线程库,目的是在提高程序的并发度的同时,降低编码难度,并在核数日益增多的 CPU 上提供更好的 scalability 和 cache locality。「M:N」是指 M 个 bthread 会映射至 N 个 pthread,一般 M 远大于 N。由于 Linux 当下的 pthread 实现(NPTL)是 1:1 的,M 个 bthread 也相当于映射至 N 个 LWP。bthread 的前身是 Distributed Process(DP)中的 fiber,一个 N:1 的合作式线程库,等价于 event-loop 库,但写的是同步代码。
bthread 是协程吗
不是。我们常说的协程特指 N:1 线程库,即所有的协程运行于一个系统线程中,计算能力和各类 event-loop 库等价。由于不跨线程,协程之间的切换不需要系统调用,可以非常快(100~200ns),受 cache 一致性的影响也小。但代价是协程无法高效地利用多核,代码必须非阻塞,否则所有的协程都被卡住,对开发者要求苛刻。
协程的这个特点使其适合写运行时间确定的 IO 服务器,典型如 http server,在一些精心调试的场景中,可以达到非常高的吞吐。但百度内大部分在线服务的运行时间并不确定,且很多检索由几十人合作完成,一个缓慢的函数会卡住所有的协程。在这点上 event-loop 是类似的:一个回调卡住整个 loop 就卡住了。比如 ubaserver(注意那个 a,不是 ubserver)是百度对异步框架的尝试,由多个并行的 event-loop 组成,真实表现糟糕:回调里打日志慢一些,访问 Redis 卡顿,计算重一点,等待中的其他请求就会大量超时,所以这个框架从未流行起来。
bthread 是一个 M:N 线程库,一个 bthread 被卡住不会影响其他 bthread。关键技术两点:work stealing 调度和 butex,前者让 bthread 更快地被调度到更多的核心上,后者让 bthread 和 pthread 可以相互等待和唤醒,这两点协程都不需要。
常见问题
bthread 是如何被调度的? pthread worker 在任何时间只会运行一个 bthread。当前 bthread 挂起时,pthread worker 先尝试从本地 runqueue 弹出一个待运行的 bthread;若没有,则随机偷另一个 worker 的待运行 bthread;仍然没有,才睡眠并会在有新的待运行 bthread 时被唤醒。
可以在 pthread worker 中阻塞么? 可以,只阻塞当前 pthread worker,其他 pthread worker 不受影响。
一个 bthread 的阻塞会影响其他 bthread 么? 不影响。若 bthread 因 bthread API 而阻塞,它会把当前 pthread worker 让给其他 bthread;若 bthread 因 pthread API 或系统函数而阻塞,当前 pthread worker 上待运行的 bthread 会被其他空闲的 pthread worker 偷过去运行。
bthread API 可以在 pthread 中调用么? 可以。bthread API 在 bthread 中被调用时影响的是当前 bthread,在 pthread 中被调用时影响的是当前 pthread,使用 bthread API 的代码可以直接运行在 pthread 中。
若有大量的 bthread 调用了阻塞的 pthread 或系统函数,会影响 RPC 运行么? 会。比如有 8 个 pthread worker,当有 8 个 bthread 都调用了系统 usleep() 后,处理网络收发的 RPC 代码就暂时无法运行了。只要阻塞时间不太长,这一般没什么影响,毕竟 worker 都用完了,除了排队也没有什么好方法。那有没有完全规避的方法呢?在 brpc 中用户可以选择调大 worker 数来缓解问题:在 server 端可设置 ServerOptions.num_threads 或 gflag -bthread_concurrency,在 client 端可设置 gflag -bthread_concurrency。
brpc 会提供类似 channel 的机制么? 不会。channel 代表的是两点间的关系,而很多现实问题是多点的,这个时候使用 channel 最自然的解决方案就是:有一个角色负责操作某件事情或某个资源,其他线程都通过 channel 向这个角色发号施令。如果我们在程序中设置 N 个角色,让它们各司其职,那么程序就能分类有序地运转下去,所以使用 channel 的潜台词就是把程序划分为不同的角色。channel 固然直观,但是有代价:额外的上下文切换——做成任何事情都得等到被调用处被调度、处理、回复,调用处才能继续,这个再怎么优化、再怎么尊重 cache locality,也是有明显开销的。另外一个现实是:用 channel 的代码也不好写。由于业务一致性的限制,一些资源往往被绑定在一起,所以一个角色很可能身兼数职,但它做一件事情时便无法做另一件事情,而事情又有优先级,各种打断、跳出、继续形成的最终代码异常复杂。
我们需要的往往是 buffered channel,扮演的是队列和有序执行的作用,bthread 提供了 ExecutionQueue,可以完成这个目的。