RPC 系列(十五):bthread 是什么

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,可以完成这个目的。

系列导航


   转载规则


《RPC 系列(十五):bthread 是什么》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
RPC 系列(十六):何时该用 bthread RPC 系列(十六):何时该用 bthread
brpc 提供了异步接口,所以一个常见的问题是:我应该用异步接口还是 bthread? 提示:简短回答——延时不高时,你应该先用简单易懂的同步接口;不行的话用异步接口;只有在需要多核并行计算时才用 bthread。 为什么不首选回调式的
2023-12-19
下一篇 
RPC 系列(十四):brpc 的线程与协程 RPC 系列(十四):brpc 的线程与协程
线程模型是服务端编程的根基。在进入 brpc 的线程设计之前,先梳理几类常见的服务端线程模型:thread-per-connection、单线程 reactor(event-loop)、N:1 线程库、多线程 reactor 与 M:N 线
2023-12-05
  目录