【免费下载链接】brpc
brpc 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".
brpc 同时提供异步访问接口和基于 M:N 线程模型的 bthread,这让很多开发者面临一个经典困惑:同一个场景到底该用同步接口、异步接口,还是 bthread?本文以官方决策指南为核心,完整讲解"延时不高用同步、不行用异步、只有多核并行计算才用 bthread"的选型原则,给出可直接套用的qps × latency判断公式与逐步示例,并结合仓库源码(bthread.h、bthread.cpp)剖析 bthread 的调度代价与适用边界,帮助你写出既简单易懂又不失性能的并发代码。
一、问题背景:brpc 提供的三种并发手段
brpc 是工业级的 C++ RPC 框架,一个服务在处理请求时往往面临三类选择:
| 手段 | 本质 | 适用场景 |
|---|---|---|
| 同步接口 | 阻塞等待 RPC 返回,代码线性、简单 | 延时低、QPS 不高时 |
| 异步接口 | 以回调代替阻塞,可获得多核扩展性 | 高并发、大量阻塞等待时 |
| bthread | M:N 线程库,M 个 bthread 映射到 N 个 pthread | 单次请求内的多核并行计算 |
bthread 是 brpc 使用的 M:N 线程库,其目标是"在提高程序并发度的同时降低编码难度,并在核数日益增多的 CPU 上提供更好的 scalability 和 cache locality"。与常见的 N:1 协程(coroutine)不同,bthread 中一个 bthread 被卡住不会影响其他 bthread,其关键技术是 work stealing 调度和 butex(让 bthread 与 pthread 可以相互等待和唤醒)。
二、同步 or 异步:用一句话给出结论
文档给出的短回答非常明确:
延时不高时你应该先用简单易懂的同步接口,不行的话用异步接口,只有在需要多核并行计算时才用 bthread。
2.1 为什么异步并不总是更好?
异步的本质是用回调代替阻塞——有阻塞的地方就有回调。虽然 JavaScript 等单线程语言中回调接受度很高、工作得很好,但它和我们在多线程 RPC 服务中需要的回调完全是两码事。区别不在于 lambda 或 future,而在于 JavaScript 是单线程的:它的回调放到多线程下可能没有一个能跑过,竞争太多。单线程的同步方法和多线程的同步方法是完全不同的。
有人会设想:能不能让服务像多个独立 eventloop 那样跑?brpc 生态中确实有过类似尝试(ubaserver,注意带 a),但实际效果糟糕,因为阻塞改回调并不简单:
- 当阻塞发生在循环、条件分支、深层子函数中时,改造特别困难;
- 大量老代码、第三方代码根本不可能改造;
- 结果代码中会出现不可避免的阻塞,导致该线程中其他回调被延迟,流量超时,server 性能不符合预期。
如果你打算"把现在的同步代码改造为大量回调",结果往往是"除了我其他人都看不太懂,并且性能可能更差了"。文档明确提醒:别被鼓吹异步的人迷惑,他们写的是"从头到尾从下到上全异步且不考虑多线程"的代码,和你要写的完全是两码事。
2.2 brpc 中的异步与单线程异步的本质区别
brpc 中的异步和单线程的异步完全不同:异步回调会运行在与调用处不同的线程中,你会获得多核扩展性,但代价是你必须意识到多线程问题。好消息是,你可以在回调中阻塞,只要线程够用,对 server 整体的性能并不会有什么影响。
异步代码依然很难写,为此 brpc 提供了组合访问(combo_channel)来简化问题。通过组合不同的 channel(如 ParallelChannel),你可以声明式地执行复杂的多路访问,而不用关心其中的回调细节。从 combo_channel.md 的源码文档看,ParallelChannel 支持同步和异步访问、可取消、可超时,还能通过CallMapper修改请求、ResponseMerger合并结果,这就是"组合优于自行维护回调计数器"的官方推荐做法。
2.3 判断使用同步或异步:qps × latency 公式
延时不长、qps 不高时,官方更建议使用同步接口——这其实也正是创建 bthread 的动机:维持同步代码的同时也能提升交互性能。
判断公式如下:
计算 qps × latency(latency 以秒为单位),如果结果和 cpu 核数是同一数量级,就用同步,否则用异步。
三个典型示例:
| 场景 | 计算过程 | 结论 |
|---|---|---|
| qps = 2000,latency = 10ms | 2000 × 0.01s = 20 | 与常见的 32 核同一数量级,用同步 |
| qps = 100,latency = 5s | 100 × 5s = 500 | 与核数不在同一数量级,用异步 |
| qps = 500,latency = 100ms | 500 × 0.1s = 50 | 基本同一数量级,可用同步;若未来延时继续增长,考虑异步 |
公式的物理含义:这个公式计算的正是同时进行的平均请求数(你可以尝试用 Little's Law 的思路证明它),它和线程数、cpu 核数是可比的。解读如下:
- 当这个值远大于 cpu 核数时,说明大部分操作并不耗费 CPU,而是让大量线程阻塞等待中——此时使用异步可以明显节省线程资源(主要是线程栈占用的内存);
- 当这个值小于或与 cpu 核数差不多时,异步能节省的线程资源就很有限了,此时简单易懂的同步代码更重要。
三、异步 or bthread:并发 RPC 与并行计算是两回事
有了 bthread 这个工具,用户甚至可以自己实现异步。以"半同步"(发起多个操作后统一等待)为例,brpc 中用户有多种选择:
- 发起多个异步 RPC 后挨个 Join(阻塞直到 RPC 结束)——注意,这仅用于和 bthread 对比,实现中官方建议使用 ParallelChannel,而不是自己 Join;
- 启动多个 bthread 各自执行同步 RPC,然后挨个 join bthreads。
3.1 并发 RPC 场景:前者效率更高
哪种效率更高?显然是前者(异步 + Join)。后者不仅要付出创建 bthread 的代价,而且在 RPC 阻塞等待期间,bthread 还被白白占用着,不能用于其他用途。
如果仅仅是为了并发 RPC,别用 bthread。
3.2 并行计算场景:bthread 的用武之地
不过当你需要并行计算时,问题就不同了。使用 bthread 可以简单地构建树形的并行计算,充分利用多核资源。比如检索过程中有三个环节可以并行处理,你可以建立两个 bthread 运行两个环节,在原地运行剩下的环节,最后 join 那两个 bthread。过程大致如下:
bool search() { ... bthread th1, th2; if (bthread_start_background(&th1, NULL, part1, part1_args) != 0) { LOG(ERROR) << "Fail to create bthread for part1"; return false; } if (bthread_start_background(&th2, NULL, 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; }对照 bthread.h 中的接口声明可以印证这段代码的两个关键 API:
bthread_start_background(tid, attr, fn, args):创建 bthreadfn(args)并把标识符写入tid,行为更接近pthread_create——调度新线程运行后立即返回(注释明确指出新线程可能比bthread_start_urgent()更晚运行);bthread_join(bt, bthread_return):使调用线程等待 bthreadbt终止,若bt已终止则立即返回(注释:"Return immediately ifbtis already terminated"),且bthread_join()不受bthread_interrupt()影响。
在 bthread.cpp 的实现中,bthread_start_background会优先尝试把新任务放入当前 pthread worker 的本地 TaskGroup(g->start_background<false>),若调用者不在 worker 线程上则走start_from_non_worker——这就是它"创建开销小、调度快"的底层来源。
这个"两线程并行 + 原地运行"实现的两个要点(原文 point):
- 线程资源更省:你当然可以建立三个 bthread 分别执行三个部分再统一 join,但相比上面这个方法,要多耗费一个 bthread 资源;
- 调度延时需要心里有数:bthread 从建立到执行是有延时的(调度延时)。在不是很忙的机器上,这个延时的中位数在 3 微秒左右,90% 在 10 微秒内,99.99% 在 30 微秒内。这带来两个推论:
- 计算时间超过 1ms 时收益才比较明显。如果计算非常简单、几微秒就结束了,用 bthread 是没有意义的;
- 尽量让原地运行的部分最慢。这样 bthread 中的部分即使被延迟了几微秒,最后可能还是会先结束,从而消除掉调度延迟的影响;并且join 一个已结束的 bthread 会立刻返回,不会有上下文切换开销。
3.3 线程池需求:用 bthread 或 ExecutionQueue
当你有类似线程池的需求——比如要执行某一类 job 的线程池——也可以用 bthread 代替。如果对 job 的执行顺序有要求,可以使用基于 bthread 的 ExecutionQueue。
从 execution_queue.md 可以看到,ExecutionQueue 提供"异步有序执行"(任务在单独线程中执行,执行顺序严格与提交顺序一致)、多生产者并发提交、取消已提交任务、stop 以及高优任务插队等能力,其任务提交接口是 wait-free 的,并且运行线程本身就是 bthread,可以在执行函数中随意使用 bthread 同步原语而不必担心阻塞 pthread 的执行——这正是它替代"自定义 job 线程池 + 队列"的底气。
四、选型决策树与最佳实践总结
综合全篇,brpc 并发代码的选型可以浓缩为下面的决策路径:
需要并发处理吗? ├─ 否 → 直接用同步接口(最简单、最易维护) └─ 是 ├─ 仅是为了并发 RPC(多个下游访问)→ 用 ParallelChannel / 异步接口, │ 不要用 bthread(qps × latency 远大于核数时异步收益明显) └─ 需要多核并行计算(CPU 密集、可拆分环节) ├─ 子任务耗时 > 1ms → bthread 树形并行(原地跑最慢的一路) ├─ 子任务耗时仅几微秒 → 不值得用 bthread └─ 需要有序执行的一类 job → ExecutionQueue几个被反复强调的关键结论,值得作为团队编码规范:
- 默认同步,按需异步:延时不高、QPS 不高时同步接口永远是第一选择,它简单、可读、可维护;
- 用公式而不是感觉做决策:
qps × latency(秒)与 cpu 核数对比,同一数量级用同步,否则用异步; - 并发 RPC 别用 bthread:并行 RPC 场景下 bthread 只会徒增创建开销,且 RPC 阻塞期间 bthread 被闲置;
- bthread 只服务于并行计算:且子任务要足够重(> 1ms)才划算,调度延时的中位数在 3 微秒左右;
- 复杂组合访问优先用 ParallelChannel,有顺序执行需求的 job 队列用 ExecutionQueue。
关于 bthread 的更多底层机制(work stealing、butex、与 pthread worker 的对应关系、阻塞时的行为),可进一步阅读 bthread.md 和 threading_overview.md;关于同步/异步接口的完整用法,见 client.md 与 combo_channel.md。需要说明的是,上述决策框架面向的是以阻塞式业务逻辑为主的通用在线服务;如果你的场景运行时间确定、代码严格非阻塞,那么 N:1 协程或 eventloop 风格也有其用武之地,但那是另一套选型逻辑了。
【免费下载链接】brpc
brpc 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".
相关推荐
brpc 并发模型选型指南:同步、异步还是 bthread?
brpc 并发模型选型指南:同步、异步还是 bthread? 这篇技术指南围绕 brpc 中最常见的架构选择题展开:当服务需要并发时,到底该用同步接口、异步接口
后端RPC框架通信网络LanceDB Python API 完整指南:同步与异步双接口实战
LanceDB Python API 完整指南:同步与异步双接口实战 LanceDB 是一个面向多模态 AI 的开发者友好型开源嵌入式向量检索库。本文基于仓库中
数据库向量数据库全文检索人工智能PyPTO 核间同步之 wait_cross_core:接口语义、同步模式与实战示例
PyPTO 核间同步之 wait_cross_core:接口语义、同步模式与实战示例 pypto_pro.language.system.wait_cross_
人工智能编译器模型编译深度学习高性能计算CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考