brpc 并发模型选型指南:同步、异步还是 bthread?
【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc
这篇技术指南围绕 brpc 中最常见的架构选择题展开:当服务需要并发时,到底该用同步接口、异步接口,还是 M:N 线程库 bthread?文章从 brpc 官方文档 bthread_or_not.md 的核心决策框架出发,结合 client.md、combo_channel.md 与 src/bthread 源码实现,给出可直接套用的量化判断公式、可复制的代码示例,以及并行计算场景下 bthread 的正确打开方式。读完你将掌握一套不依赖直觉、可量化、可验证的并发模型选型方法论。
选型问题的本质:三种并发工具,各自解决什么问题
brpc 提供了 异步接口,又提供了 bthread,因此一个高频问题随之而来:我应该用异步接口还是 bthread?
要回答这个问题,首先要厘清三者各自的定位:
- 同步接口:延续传统的"阻塞直到 RPC 返回"的编程模型,代码最简单、最容易理解,但一次调用会占住一个执行体;
- 异步接口:通过回调(done)代替阻塞,CallMethod 发出 request 后立即返回,让出执行体以承载更高并发;
- bthread:brpc 使用的 M:N 线程库,M 个 bthread 映射到 N 个 pthread(一般 M 远大于 N),可以在同步的编程模型下实现并行计算。
brpc 的官方结论非常直白:
延时不高时你应该先用简单易懂的同步接口,不行的话用异步接口,只有在需要多核并行计算时才用 bthread。
这句话是整篇选型指南的骨架,下面逐一展开其背后的判断依据。
同步还是异步:用 qps × latency 公式做量化判断
为什么不能盲目追随"全异步"风格
异步的本质是"用回调代替阻塞,有阻塞的地方就有回调"。javascript 这类语言中回调大行其道,接受度很高,但很多鼓吹异步的人写的代码"是从头到尾从下到上全异步且不考虑多线程的",和真实业务代码是两码事。关键差异在于:
- javascript 是单线程的,回调之间没有竞争;
- 而 brpc 的异步是多线程的,异步回调会运行在与调用处不同的线程中,你获得多核扩展性的代价,是必须时刻意识到多线程问题。
那能不能把服务组织成"多个线程、每个都是独立 eventloop"的形式呢?可以,但实际效果糟糕:当阻塞发生在循环、条件分支、深层子函数中时,改造成本极高,老代码和第三方代码往往根本没法改。结果就是代码中不可避免出现阻塞,导致那个线程中其他回调都被延迟,流量超时,server 性能反而不符合预期。
brpc 异步的真实面貌
brpc 中的异步和单线程异步完全不同:异步回调会运行在与调用处不同的线程中。这意味着:
- 你会在回调中获得真正的多核扩展性;
- 只要线程够用,你可以在回调中阻塞,对 server 整体性能并无大碍。
从 client.md 的实现说明可以看到,异步访问是给 CallMethod 传递一个额外的回调对象 done,CallMethod 发出 request 后即结束,当 server 返回 response 或发生错误(包括超时)时,done->Run() 才会被调用,后续处理都应写在 done->Run() 里。由于 CallMethod 结束不意味着 RPC 结束,response/controller 一般得创建在堆上并在 done->Run() 中删除。
不过异步代码终究难写,所以 brpc 提供了 组合访问:通过组合不同的 Channel,你可以声明式地执行复杂访问而不用关心细节。典型的如 ParallelChannel,同时访问其包含的 sub channel 并合并结果,支持同步和异步访问、发起异步操作后可立刻删除、可取消、支持超时,还可用fail_limit/success_limit控制提前结束条件。
判断公式:计算同时进行的平均请求数
brpc 给出的判断标准是一个可量化的公式:
判断使用同步或异步:计算
qps * latency(in seconds),如果和 CPU 核数是同一数量级,就用同步,否则用异步。
文档给出了三个具体算例:
| 场景 | qps | latency | 计算结果 qps × latency | 结论 |
|---|---|---|---|---|
| 高 QPS 低延时 | 2000 | 10ms | 2000 × 0.01s = 20 | 与常见 32 核同数量级,用同步 |
| 低 QPS 高延时 | 100 | 5s | 100 × 5s = 500 | 与核数不在同一数量级,用异步 |
| 中等场景 | 500 | 100ms | 500 × 0.1s = 50 | 基本同数量级,可用同步;延时若继续增长再考虑异步 |
这个公式计算的是同时进行的平均请求数(可用 Little's Law 类似思路推导:在途请求数 = 到达率 × 平均服务时间)。它和线程数、CPU 核数是可比的:
- 当这个值远大于 CPU 核数时,说明大部分操作并不耗费 CPU,而是大量线程在阻塞等待。此时使用异步可以明显节省线程资源(主要是栈占用的内存);
- 当这个值小于或和核数差不多时,异步能节省的线程资源很有限,这时"简单易懂的同步代码"更重要。
异步还是 bthread:并发 RPC 别用 bthread,并行计算才用
有了 bthread 这个工具,用户甚至可以自己实现异步。文档以"半同步"为例,展示了两种实现路径:
- 发起多个异步 RPC 后挨个
Join,该函数会阻塞直到 RPC 结束; - 启动多个 bthread 各自执行同步 RPC,然后挨个 join bthreads。
哪种效率更高?显然是前者。后者不仅要付出创建 bthread 的代价,而且在 RPC 过程中 bthread 还被阻塞着,不能用于其他用途。
如果仅仅是为了并发 RPC,别用 bthread。
这条结论与 client.md 中"半同步"的实现思路一致:用Join实现"等待多个异步访问完成",因为调用处会等到所有 RPC 都结束后再醒来,controller 和 response 都可以放栈上。更重要的是,文档明确指出:实现中建议你使用 ParallelChannel,而不是自己 Join——ParallelChannel 把"并发发起 + 合并结果"封装成了声明式的 Channel 组合,避免手写多线程回调的各类陷阱(如生命周期、取消、再组合等问题,combo_channel.md 开篇有系统论述)。
并行计算才是 bthread 的主场:树形并行
当需要并行计算时,问题就完全不同了。使用 bthread 可以简单地构建树形的并行计算,充分利用多核资源。以检索过程为例,三个环节可以并行处理:建立两个 bthread 运行其中两个环节,在原地运行剩下的环节,最后 join 那两个 bthread:
bool search() { ... bthread th1, th2; if (bthread_start_background(&th1, nullptr, part1, part1_args) != 0) { LOG(ERROR) << "Fail to create bthread for part1"; return false; } if (bthread_start_background(&th2, nullptr, part2, part2_args) != 0) { LOG(ERROR) << "Fail to create bthread for part2"; return false; } part3(part3_args); bthread_join(th1); bthread_join(th2); return true; }这个写法的两个关键 point:
- 节省一个线程资源:你当然可以建立三个 bthread 分别执行三个部分再 join 它们,但相比本方法要多耗费一个线程资源(bthread 也是要占栈、要调度的);
- 调度延时可控且可被掩盖:bthread 从建立到执行是有延时的(调度延时)。在不是很忙的机器上,这个延时的中位数在3 微秒左右,90% 在10 微秒内,99.99% 在30 微秒内。这给出两个实操推论:
- 计算时间超过 1ms 时收益比较明显。如果计算非常简单,几微秒就结束了,用 bthread 没有意义;
- 尽量让原地运行的部分最慢。这样 bthread 中的部分即使被延迟几微秒,最后可能还是会先结束,从而消除掉延迟的影响;并且
bthread_join一个已结束的 bthread 时会立刻返回,不会有上下文切换开销。
源码级印证:API 行为与调度模型
上述代码用到的两个核心 API 定义在 src/bthread/bthread.h:
bthread_start_background(tid, attr, fn, args)(bthread.h#L67-L70):创建 bthread,行为接近pthread_create——把新线程放入调度队列后即返回,新线程可能比bthread_start_urgent()更晚运行。这正是"从建立到执行有调度延时"的接口层面原因;bthread_join(bt, bthread_return)(bthread.h#L116-L124):等待 bthread 终止,若已终止则立即返回——这正是"join 已结束的 bthread 立刻返回、无上下文切换开销"的实现依据。注意*bthread_return始终被置为 null,如需回传结果,应通过创建时传入的 args 传递。
为什么 bthread 能支撑这种"原地等待并行结果"的模型?从 bthread.md 的 FAQ 与源码结构可以确认两点关键技术:
- Work stealing 调度:pthread worker 在任何时间只运行一个 bthread;当前 bthread 挂起时,worker 先尝试从本地 runqueue 弹出待运行的 bthread,没有则随机偷其他 worker 的,仍没有才睡眠等待唤醒。这保证了被创建出来的 bthread 能快速被空闲核心调度;
- butex(bthread mutex/condition 底层原语):让 bthread 和 pthread 可以相互等待和唤醒,实现 src/bthread/butex.cpp 等。
bthread 是 M:N 线程库,一个 bthread 被卡住不会影响其他 bthread;bthread 中也可以调用阻塞的 pthread 或系统函数,只会阻塞当前 pthread worker,其余 worker 不受影响。但要注意:若有大量 bthread 同时阻塞(比如 8 个 worker 全被 usleep 占住),处理网络收发的 RPC 代码会暂时无法运行,可以通过调大 worker 数缓解,server 端可设置ServerOptions.num_threads或-bthread_concurrency(详见 server.md)。
类线程池需求:用 ExecutionQueue 替代
除了树形并行计算,当你有类似线程池的需求(比如执行一类 job 的线程池)时,也可以用 bthread 代替。如果对 job 的执行顺序有要求,brpc 提供了基于 bthread 的 ExecutionQueue:它承担"队列 + 有序执行"的角色,多个线程可以向队列提交任务,任务以确定的顺序被执行。
之所以推荐 ExecutionQueue 而非自己造 channel,bthread.md 的 FAQ 给出了设计层面的解释:channel 代表两点间的关系,而很多现实问题是多点的,用 channel 的自然方案是划分角色、互相发号施令,这会引入额外的上下文切换开销,且"做成任何事情都得等到被调用处被调度、处理、回复",再优化也有明显开销。ExecutionQueue 正好补上了这个"buffered channel 扮演队列和有序执行"的位置。
小结:一套完整的三步选型决策
综合 bthread_or_not.md 的完整脉络,可以沉淀为三步决策流程:
- 先算账再选同步/异步:计算
qps × latency(in seconds),与 CPU 核数比较。同一数量级用同步(代码简单易懂优先),远大于核数用异步(节省被阻塞线程的栈内存); - 并发 RPC 一律别上 bthread:要并发发起多个 RPC,优先用 ParallelChannel 这类组合 Channel,用声明式组合替代手写回调与 Join;
- 只有真正的多核并行计算才用 bthread:计算量超过 1ms、需要并行跑多个环节时,用
bthread_start_background+ 原地执行 +bthread_join构建树形并行,并把最慢的环节留在原地执行以掩盖调度延时;有顺序要求的任务队列则交给 ExecutionQueue。
brpc 的设计哲学也印证了这一点:在 bthread.md 的 FAQ 中,官方明确建议"除非你需要在一次 RPC 过程中让一些代码并发运行,你不应该直接调用 bthread 函数,把这些留给 brpc 做更好"。选型不是追求最潮的技术,而是让每一份并发能力都落在它最能发挥价值的位置上。
【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考