尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

brpc bthread 完全解析:M:N 线程库的设计目标、调度原理与工程实践

发布时间:2026/9/13 23:04:34

资讯中心
01
ARTICLE

brpc bthread 完全解析:M:N 线程库的设计目标、调度原理与工程实践

brpc bthread 完全解析:M:N 线程库的设计目标、调度原理与工程实践
brpc bthread 完全解析M:N 线程库的设计目标、调度原理与工程实践【免费下载链接】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/brpcbrpc 的 bthread 是一个工业级的 M:N 线程库M 个用户态线程bthread映射到 N 个 pthread worker 之上一般 M 远大于 N让开发者继续使用同步编程的直觉写法却能同时获得高并发、多核扩展性与更好的 cache locality。本文以官方文档 docs/cn/bthread.md 为主线结合 src/bthread 的真实源码实现逐层讲清 bthread 的定位、目标与边界、work stealing 调度与 butex 同步两大核心技术并回答从能否在 bthread 里调用阻塞函数到为什么没有 Channel等一系列高频问题最后给出 worker 数调优与规避全员阻塞的实战方案。bthread 是什么从 fiber 到 M:N 线程库相关文档docs/cn/bthread.md英文版brpc 中的 bthread 是M:N 线程库M 个 bthread 会被映射到 N 个 pthread 上通常 M 远大于 N。由于 Linux 当下的 pthread 实现NPTL是 1:1 的因此 M 个 bthread 在底层也等价于映射到 N 个 LWP轻量级进程。bthread 的前身是百度分布式处理框架Distributed ProcessDP中的 fiber——一个N:1 的合作式线程库。N:1 模型等价于 event-loop 库区别在于用户写的是同步代码把事件回调替换为上下文栈、寄存器把运行回调替换为跳转至上下文。关于各线程模型的横向对比可以参考 docs/cn/threading_overview.md。设计目标Goals用户可以延续同步编程模式能在数百纳秒内创建一个 bthread并可用多种原语mutex、cond、rwlock、semaphore 等同步bthread 的所有接口都可在 pthread 中被调用且行为合理使用 bthread API 的代码可以直接运行在 pthread 中能充分利用多核更好的 cache locality支持 NUMA 是加分项。从源码看这些目标对应 src/bthread/bthread.h 中完整的 API 家族创建类bthread_start_urgent/bthread_start_background、调度类bthread_usleep/bthread_yield/bthread_interrupt、同步原语bthread_mutex_*、bthread_cond_*、bthread_rwlock_*、bthread_sem_*、线程局部存储bthread_key_create/bthread_setspecific、以及bthread_join、bthread_stop、bthread_once等。明确拒绝的目标NonGoals不提供 pthread 兼容接口链接即用bthread 没有优先级不适用于所有场景链接替换的方式容易让用户在不知情的情况下误用 bthread 而产生 bug。不拦截所有可能阻塞的 glibc 函数与系统调用理由有三——bthread 阻塞可能切换底层系统线程依赖系统 TLS 的函数行为未定义与阻塞 pthread 的函数混用时可能死锁hook 函数本身效率通常更差往往需要额外的系统调用如 epoll。这种覆盖对 N:1 合作式线程库fiber有一定意义虽然函数本身变慢了但不覆盖的话系统线程阻塞会导致所有 fiber 一起停摆。不修改内核让 pthread 支持同核快速切换一旦拥有大量 pthread每个线程对资源的诉求被稀释基于 thread-local cache 的代码如 tcmalloc效果会很差而独立的 bthread 最终被映射到少量 pthread 上不会有这个问题。bthread 相比 pthread 的性能提升很大一部分正来自更集中的线程资源。另一个考量是可移植性——bthread 更倾向纯用户态代码。核心技术一work stealing 调度当 bthread 因 bthread API 阻塞时它会把当前 pthread worker 让给其他 bthread 运行。调度器的核心是work stealing工作窃取每个 pthread worker 在任何时刻只运行一个 bthread当前 bthread 挂起后worker 先尝试从自己的本地 runqueue 弹出待运行 bthread若本地为空则随机偷另一个 worker 的待运行 bthread仍然没有才睡眠并在出现新的待运行 bthread 时被唤醒。源码印证TaskGroup 与本地 runqueue在 src/bthread/task_group.h 中TaskGroup是线程局部的任务组持有WorkStealingQueuebthread_t _rq本地 runqueue也就是自己优先取走的队列RemoteTaskQueue _remote_rq远程任务队列接收来自其他线程投递的任务_main_tid/_main_stack该 worker 上主任务的标识与栈_plParkingLotworker 空闲时的停车/唤醒设施。TaskGroup::sched()负责挂起当前任务并运行下一个 bthreadsteal_task()的流程是src/bthread/task_group.h#L333-L341先尝试从_remote_rq弹出任务失败后再调用_control-steal_task()去随机窃取其他 worker 的任务。TaskControlsrc/bthread/task_control.h则管理所有 TaskGroup其中的steal_task(tid, seed, offset)正是随机偷一个 group 的 runqueue的实现worker_thread()是每个 worker pthread 的入口循环。本地队列采用 src/bthread/work_stealing_queue.h 中的WorkStealingQueuepush/pop只在所属 worker 上串行执行steal可以与push/pop并行通过_bottom/_top索引与原子操作实现无锁化——这正是本地取走几乎无竞争、跨核窃取才需要原子操作的 cache-friendly 设计。steal()中还特别注释了为性能考虑允许误报空队列false negative。关键效果一个 bthread 卡住不会影响其他 bthread若它因 bthread API 阻塞会把 worker 让给其他 bthread若因 pthread API 或系统函数阻塞该 worker 上待运行的 bthread 会被其他空闲 worker 偷走运行。bthread 之间切换是纯用户态上下文切换src/bthread/context.cpp 负责栈与寄存器切换无需系统调用因此能实现文档所述的数百纳秒内创建 bthread。TaskGroup中还实现了 CPU 时间统计CPUTimeStat/AtomicInteger128见 src/bthread/task_group.h#L239-L300配合bthread_cpu_clock_ns()可以精确度量单个 bthread 消耗的 CPU 时间需开启bthread_enable_cpu_clock_stat标志。核心技术二butex——bthread 与 pthread 互相等待唤醒的基石butex 是 bthread 的 futex 类原语一个 32 位的同步原语用于 bthread/pthread 之间的相互等待与唤醒。这正是文档中强调的、协程不需要的第二个关键技术——它让bthread 阻塞时把系统线程让出去成为可能也让 pthread 与 bthread 可以安全地互相等待。src/bthread/butex.h 定义了完整 APIbutex_create()/butex_destroy()创建/销毁一个 futex 风格的 32 位变量注意所有 butex 都是进程内私有的不能跨进程butex_wake()/butex_wake_n()/butex_wake_all()/butex_wake_except()唤醒等待者返回实际唤醒的线程数butex_requeue()唤醒 butex1 上至多一个线程并让其余等待者转移到 butex2 上等待butex_wait(butex, expected_value, abstime, prepend)原子地检查*butex expected_value满足则挂起直到被唤醒或到达绝对时间abstime否则立即返回。注意它使用的是绝对时间区别于 FUTEX_WAIT 的相对时间。常量MIN_SLEEP_US 2表明若线程预计挂起少于 2 微秒直接返回ETIMEDOUT——因为睡眠不足 2 微秒既低效又无用。底层实现位于 src/bthread/sys_futex.cpp最终通过 Linuxfutex系统调用完成真正的挂起与唤醒并配合 ParkingLotsrc/bthread/parking_lot.h管理 worker 的空闲与唤醒。bthread_mutex_*、bthread_cond_*、bthread_rwlock_*、bthread_sem_*分别见 src/bthread/mutex.cpp、src/bthread/condition_variable.cpp、src/bthread/rwlock.cpp、src/bthread/semaphore.cpp都是建立在 butex 之上的同步原语因此它们阻塞的只是当前 bthread而不是整个系统线程。bthread 是协程吗不是。我们常说的协程特指 N:1 线程库即所有协程运行于一个系统线程中计算能力和各类 event-loop 库等价。由于不跨线程协程切换不需要系统调用可以非常快约 100ns–200ns受 cache 一致性影响也小但代价是协程无法高效利用多核代码必须非阻塞否则一个缓慢的函数会卡住所有协程对开发者要求苛刻。协程的这个特点使其适合编写运行时间确定的 IO 服务器典型如 http server在精心调试的场景中能达到很高的吞吐。但大多数在线服务的运行时间并不确定且检索逻辑常由几十人合作完成一个缓慢的函数就会卡住所有协程。event-loop 同理一个回调卡住整个 loop 就卡住了。文档以 ubaserver注意那个 a为例——百度对异步框架的尝试由多个并行的 event-loop 组成真实表现糟糕回调里打日志慢一些、访问 redis 卡顿、计算重一点等待中的其他请求就会大量超时因此这个框架从未流行起来。bthread 是M:N 线程库一个 bthread 被卡住不会影响其他 bthread。区别正是前面两项技术work stealing 调度让 bthread 更快地被调度到更多核心上butex让 bthread 与 pthread 可以相互等待和唤醒——这两点都是协程不需要的。更多线程模型知识参见 docs/cn/threading_overview.md。高频 FAQbthread 的使用边界与行为语义Q我应该在程序中多使用 bthread 吗不应该。除非你需要在一次 RPC 过程中让一些代码并发运行否则不要直接调用 bthread 函数把这些留给 brpc 做更好。brpc 自身在服务端将每个请求运行在 bthread 中客户端也基于 bthread 实现了同步访问的并发能力用户代码直接使用 bthread API 的场景其实很有限。Qbthread 和 pthread worker 如何对应一个 pthread worker 在任何时间只会运行一个 bthread。当前 bthread 挂起时worker 先尝试从本地 runqueue 弹出待运行的 bthread若没有则随机偷另一个 worker 的待运行 bthread仍然没有才睡眠并会在有新的待运行 bthread 时被唤醒。这段描述与 src/bthread/task_group.h 中wait_task()/steal_task()的实现完全一致。Qbthread 中能调用阻塞的 pthread 或系统函数吗可以只阻塞当前 pthread worker其他 pthread worker 不受影响。这正是 M:N 模型相对 N:1 模型的本质优势——用户代码中残留的阻塞调用不会拖垮整个进程。Q一个 bthread 阻塞会影响其他 bthread 吗不影响。若 bthread 因 bthread API 阻塞它会把当前 pthread worker 让给其他 bthread通过TaskGroup::sched()切换到下一个任务若因 pthread API 或系统函数阻塞当前 worker 上待运行的 bthread 会被其他空闲 worker 偷走运行。Qpthread 中可以调用 bthread API 吗可以。bthread API 在 bthread 中被调用时影响的是当前 bthread在 pthread 中被调用时影响的是当前 pthread。因此使用 bthread API 的代码可以直接运行在 pthread 中例如在普通线程里创建 bthread、使用bthread_mutex_lock等。这对应 src/bthread/bthread.h 中Every bthread API is callable from a pthread的设计目标。线程局部存储 APIbthread_setspecific/bthread_getspecific同样如此在 bthread 中取 bthread 局部数据在 pthread 中取 pthread 局部数据。Q若有大量 bthread 调用了阻塞的 pthread 或系统函数会影响 RPC 运行么会。例如有 8 个 pthread worker当 8 个 bthread 都调用了系统usleep()后处理网络收发的 RPC 代码就暂时无法运行了。只要阻塞时间不太长这一般没什么影响——毕竟 worker 都用完了除了排队也没有更好的办法。brpc 中用户可以选择调大 worker 数来缓解server 端设置 ServerOptions.num_threads 或-bthread_concurrencyclient 端设置-bthread_concurrency。worker 线程数如何设置num_threads 与 -bthread_concurrency从 src/bthread/bthread.cpp#L47-L54 可以看到-bthread_concurrency的定义默认值为8 BTHREAD_EPOLL_THREAD_NUM含义是 Number of pthread workers。另有-bthread_min_concurrency默认 0当其为非正数时不做懒加载worker 会按-bthread_concurrency与bthread_setconcurrency()的值立即创建为正数时则先按该值创建不足时按需补充。对应运行时 API 为bthread_getconcurrency()/bthread_setconcurrency()注意创建过 bthread 之后不能再调低 concurrency。在 brpc 服务端设置ServerOptions.num_threads即可默认是 CPU 核数含超线程。但需要特别注意 docs/cn/server.md#worker线程数 中强调的两点ServerOptions.num_threads仅仅是个提示不能认为 server 就用了这么多线程因为进程内所有 Server 和 Channel 会共享线程资源实际 worker 线程数 max(所有ServerOptions.num_threads,-bthread_concurrency)。例如一个进程内有两个 Servernum_threads分别为 24 和 36bthread_concurrency为 16那么 worker 线程数为 max(24, 36, 16) 36——这与很多其他 RPC 实现加起来的做法不同。Channel 没有对应选项client 端只能通过-bthread_concurrency调整。此外 brpc不区分 IO 线程和处理线程它自己编排 IO 与处理代码以获得更高的并发度和线程利用率这也是为什么所有 worker 被阻塞会直接波及网络收发。完全规避所有 worker 被阻塞的几种思路文档给出了四条路径层层递进动态增加 worker 数不推荐当大量 worker 同时被阻塞时它们很可能在等待同一个资源比如同一把锁增加 worker 可能只是增加了更多等待者实际未必如意。区分 IO 线程和 worker 线程治标不治本IO 线程专门收发、worker 线程执行用户逻辑即使 worker 全部阻塞也不影响 IO。但增加一层处理环节并不能缓解拥塞——worker 全部卡住时程序仍然卡住只是卡的位置从 socket 缓冲转移到了 IO 线程与 worker 线程之间的消息队列换言之worker 卡住时还在运行的 IO 线程做的可能是无用功这正是上文没什么影响真正的含义。另一个代价是每个请求都要从 IO 线程跳转至 worker 线程多一次上下文切换机器繁忙时切换可能无法被及时调度导致更多延时长尾。限制最大并发推荐可落地见 docs/cn/server.md#限制最大并发。只要同时被处理的请求数低于 worker 数自然就不会出现所有 worker 被阻塞的情况。brpc 支持 server 级ServerOptions.max_concurrency0 代表不限制和 method 级server.MaxConcurrencyOf(...)两种限制当超过限制时会立刻给 client 回复brpc::ELIMIT错误而不是排队client 收到后应重试另一台 server。最大并发度可用 Littles law 估算最大并发 ≈ 极限 QPS × 低负载延时。还可以把 method 的最大并发设为auto使用自适应限流算法详见 docs/cn/auto_concurrency_limiter.md。被阻塞 worker 超过阈值时切换执行方式pthread 模式已实现当被阻塞的 worker 超过阈值比如 8 个中的 6 个时不再在原地调用用户代码而是扔到一个独立的线程池中运行这样即使用户代码全部阻塞也总能保留几个 worker 处理 RPC 收发。目前 bthread 模式没有这个机制但类似机制在打开 pthread 模式时-usercode_in_pthread已被实现。该机制更多是为了规避极端情况下的死锁比如所有用户代码都 lock 在一个 pthread mutex 上而这个 mutex 需要在某个 RPC 回调中 unlock——如果所有 worker 都被阻塞就没有线程处理 RPC 回调整个程序死锁。虽然绝大部分 RPC 实现都有这个潜在问题但实际出现频率很低只要养成不在锁内做 RPC的好习惯完全可以规避。bthread 会有 Channel 吗——为什么答案是不会文档明确回答不会。理由分两层模型不匹配channel 代表的是两点间的关系而很多现实问题是多点的。使用 channel 最自然的解决方案是有一个角色负责操作某件事情或某个资源其他线程都通过 channel 向这个角色发号施令若设置 N 个角色各司其职程序就能分类有序地运转。所以使用 channel 的潜台词是把程序划分为不同的角色。channel 固然直观但有代价——额外的上下文切换做成任何事情都要等被调用处被调度、处理、回复调用处才能继续再怎么优化、再怎么尊重 cache locality 也有明显开销。代码难写由于业务一致性的限制一些资源往往被绑定在一起一个角色很可能身兼数职但它做一件事时便无法做另一件事而事情又有优先级各种打断、跳出、继续形成的最终代码异常复杂。我们真正需要的往往是 buffered channel——它扮演的是队列和有序执行的作用。bthread 提供了 ExecutionQueue 来完成这个目的。从源码看src/bthread/execution_queue.h 正是 bthread 生态中生产者投递任务、消费者有序执行的高性能队列实现是 Channel 场景的官方替代品。什么时候才该用 bthread与同步/异步的取舍结论前置见 docs/cn/bthread_or_not.md延时不高时先用简单易懂的同步接口不行的话用异步接口只有在需要多核并行计算时才用 bthread。判断同步或异步的经验公式计算qps × latency(秒)如果和 CPU 核数是同一数量级就用同步否则用异步。例如 qps2000、latency10ms 时乘积为 20与常见 32 核同数量级用同步qps100、latency5s 时乘积为 500远大于核数用异步。这个公式算的是同时进行的平均请求数Littles law当它远大于核数时说明大量线程只是阻塞等待而非耗 CPU异步能明显节省线程栈内存。如果只是为了并发 RPC别用 bthread——发起多个异步 RPC 后逐个 join或直接用ParallelChannel比起多个 bthread 各自做同步 RPC更高效后者既要付出创建 bthread 的代价RPC 过程中 bthread 还被阻塞着无法复用。需要并行计算时才用 bthread可以简单地构建树形并行计算。比如检索中三个环节可并行处理建立两个 bthread 跑两个环节、原地跑剩下的环节最后 joinbool 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; }这样做的要点一是相比三个 bthread 分别执行再统一 join少消耗一个线程资源二是要理解 bthread 从创建到执行存在调度延时——在不太忙的机器上这个延时中位数约 3 微秒、90% 在 10 微秒内、99.99% 在 30 微秒内。由此得到两条实践原则计算时间超过 1ms 时收益才明显几微秒就结束的简单计算用 bthread 没有意义尽量让原地运行的部分最慢那样 bthread 中的部分即使被延迟几微秒最终可能还是先结束从而消除延迟影响且 join 一个已结束的 bthread 会立刻返回没有上下文切换开销。另外当你有类似执行一类 job 的线程池需求时也可以用 bthread 代替若对 job 的执行顺序有要求则应使用基于 bthread 的 ExecutionQueue。小结bthread 是 brpc 高性能的根基之一它的价值不在于让用户到处创建线程而在于让同步编程模型在高并发、多核场景下重新变得可行work stealing 调度解决了多核扩展性与 cache localitybutex 解决了 bthread 与 pthread 之间的互等互醒M:N 映射让线程资源高度集中这正是相对 pthread 的性能红利来源。理解它的目标、边界与 FAQ 中的行为语义是正确使用 brpc 并做出合理并发架构决策的前提。进一步阅读可参考 docs/cn/threading_overview.md线程模型全景、docs/cn/bthread_or_not.mdbthread 使用时机、docs/cn/execution_queue.mdChannel 替代品以及 docs/cn/server.mdworker 数与并发控制实战。【免费下载链接】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),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。