Boost.Asio 线程模型对比:单 io_context + strand vs 多 io_context + 单线程
少量线程 + 任意多虚拟串行通道”,胜在效率、灵活、API 标准、有 work stealing,输在隔离弱、心智模型重、调度不确定。适合:串行单位多、负载分布动态、追求并发吞吐的系统。典型场景:网络服务器、消息分发层。“线程数 = 关键串行单位数,各自跑各自的”,胜在强隔离、易监控、cache 友好、心智简单,输在没有 work stealing、资源开销大、扩展性差。适合:实时关键路径、负载分
Boost.Asio 线程模型对比:单 io_context + strand vs 多 io_context + 单线程
strand 不是线程池,它是
io_context上的"逻辑串行视图" —— 同一 strand 内的 handler 串行,不同 strand 之间(以及 strand 与 raw post 之间)仍然并行,共享同一组 worker 线程。
与之相对的另一种模式是 “多个独立 io_context,每个池子只跑一根线程” —— 物理上就把串行单位与线程绑死,牺牲负载均衡换取强隔离、可观测性和心智简单。
本文把两种线程组织方式对照清楚:它们各自擅长什么、什么时候是坑、以及实际项目里怎么混合使用。
1. 先给一个共同的问题:把串行和并行同时表达出来
在使用 boost::asio 写并发代码时,经常会遇到这样的需求:
- 某一类工作必须串行:写同一份共享状态(例如同一个分片的内存仓库)、按顺序处理同一连接上的消息、对同一资源的访问要互斥。
- 不同类工作之间要并发:不同分片之间的写入互不相干、不同连接的处理是独立的、收尾任务与主路径不该互相阻塞。
boost::asio 提供两条主流的实现路径:
- A. 单 io_context + 多线程 + strand:用
strand给"逻辑串行单位"分组。 - B. 多 io_context + 每池 1 个线程:每个串行单位有一个独立
io_context,该io_context上只跑一根线程。
两者结果语义都能做到"同 key 串行 + 不同 key 并行",但工程属性差别很大。下面一项项对比。
2. 两种模型的结构图
2.1 模式 A:单 io_context + strand
┌────────────────────────────┐
│ boost::asio::io_context │
│ (一个共享任务队列) │
└────────┬───────────────────┘
│
┌──────────┬─────┴──────┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
worker_0 worker_1 worker_2 worker_3 worker_N
(谁先空闲谁就抢下一个 ready handler)
串行化通过 strand 表达:
strand_A ──► 该 strand 内同一时刻最多一个 handler 在跑
strand_B ──► 与 A 互相独立,可以与 A 并行
无 strand ──► 完全并行(任意 worker 抢)
关键性质:
- 所有任务进同一个队列,由 worker 池抢着执行(work stealing 由 asio 内部处理)。
strand不持有线程,它是一种"调度装饰器":保证投到同一 strand 的 handler 不会并行执行。- 不同 strand 之间(以及 strand 与无 strand 的 post 之间)互相不互斥,完全可以并行。
2.2 模式 B:多 io_context + 每池 1 个线程
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ io_context 0 │ │ io_context 1 │ ... │ io_context N │
│ + thread 0 │ │ + thread 1 │ │ + thread N │
└──────────────┘ └──────────────┘ └──────────────┘
▲ ▲ ▲
│ │ │
└──────── 按 key 路由到对应 io_context ──────┘
注意:不同 io_context 之间没有 work stealing。
每个池子只有 1 个线程,该线程只服务自己池子里的任务。
关键性质:
- N+1 个独立的
io_context,每个池子启单线程事件循环。 - 业务侧用一层路由把任务静态分桶到对应池子(例如
key % N)。 - 单池内只有一根线程 → 天然串行,无锁。不同池之间 → 物理隔离,互不抢线程。
3. 两个最小可运行示例
3.1 模式 A:strand 写法
#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <vector>
int main() {
boost::asio::io_context io;
auto guard = boost::asio::make_work_guard(io);
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i)
workers.emplace_back([&io] { io.run(); });
auto strandA = boost::asio::make_strand(io);
auto strandB = boost::asio::make_strand(io);
boost::asio::post(strandA, []{ std::cout << "A1\n"; });
boost::asio::post(strandA, []{ std::cout << "A2\n"; });
boost::asio::post(strandB, []{ std::cout << "B1\n"; });
boost::asio::post(io.get_executor(), []{ std::cout << "raw\n"; });
guard.reset();
for (auto& t : workers) t.join();
}
- A1 → A2 一定串行执行(可能在不同 worker 上,但不会并发)。
- B1 可以与 A 系列并行。
raw任务可由任意空闲 worker 立刻执行。
3.2 模式 B:多 io_context 单线程写法
#include <boost/asio.hpp>
#include <array>
#include <memory>
#include <thread>
struct Pool {
boost::asio::io_context io;
std::thread th;
std::unique_ptr<boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>> guard;
};
constexpr int N = 4; // N 个 key 桶 + 1 个兜底桶
std::array<Pool, N + 1> pools;
void start_pools() {
for (auto& p : pools) {
p.guard = std::make_unique<boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>>(
boost::asio::make_work_guard(p.io));
p.th = std::thread([&p]{ p.io.run(); });
}
}
template <typename Fn>
void post_by_key(int key, Fn&& fn) {
boost::asio::post(pools[key % N].io.get_executor(), std::forward<Fn>(fn));
}
template <typename Fn>
void post_global(Fn&& fn) {
boost::asio::post(pools[N].io.get_executor(), std::forward<Fn>(fn));
}
int main() {
start_pools();
post_by_key(0, []{ /* 落在 pools[0],只能在 thread 0 上跑 */ });
post_by_key(1, []{ /* 落在 pools[1] */ });
post_by_key(5, []{ /* 5 % 4 == 1,落在 pools[1] */ });
post_global( []{ /* 落在 pools[N],与所有 key 桶物理隔离 */ });
for (auto& p : pools) p.guard.reset();
for (auto& p : pools) p.th.join();
}
- 同 key 永远落在同一线程 → 串行 + cache 友好。
- 不同 key 桶之间互不抢线程。
- 没有 work stealing:
pools[0]卡死,pools[1..N]闲着也帮不上忙。
4. strand 究竟在做什么
很多人对 strand 的第一印象是"它是个 mutex"或者"它是个小线程池",这两个都不准确。准确的描述是:
strand 是
io_context上的一种 executor 适配器,它把"我"上面收到的所有 handler 排成一个队列,保证任意时刻最多一个 handler 处于执行中。
几个推论:
- 同一 strand 内 → 串行,handler 之间天然有"happens-before",无需互斥锁。
- 不同 strand 之间 → 并行,strand 不提供跨 strand 的同步保证。
- strand 与 raw post(同一 io_context) → 并行。
- strand 不占线程:它只是个"调度规则",真正干活的还是 io_context 后面的 worker 池。
- strand 的执行线程不固定:同一 strand 的两次 handler 可能在不同 worker 上跑,但它们绝不会重叠。
代码层面常见用法:
// 把 handler 绑定到 strand
boost::asio::post(strandA, []{ /* ... */ });
// 给异步操作的完成回调绑定 strand
socket.async_read_some(buf, boost::asio::bind_executor(strandA,
[](auto ec, std::size_t n) { /* 在 strandA 上跑 */ }));
注意一个常踩的语义差异:
boost::asio::post(strand, fn):永远排队,即使当前线程已经在该 strand 上,也会先返回再异步执行。boost::asio::dispatch(strand, fn):如果当前线程已经在该 strand 上,会立即同步调用;否则才排队。
这是 strand 上经典的"重入陷阱"之一 —— dispatch 会绕过你以为存在的"先返回再执行"的排队语义。
5. 详细对比表
| 维度 | A. 单 io_context + strand | B. 多 io_context + 单线程 |
|---|---|---|
| 任务调度 | 共享队列,worker 抢任务,自带 work stealing | 任务按 key 静态路由,不同池之间无 work stealing |
| 串行化粒度 | 用 strand 定义任意多"逻辑串行通道" |
一个池 = 一个"物理串行通道" |
| 同串行单位内 | strand 内串行,handler 之间无锁 | 池内只有 1 线程,自然串行,无锁 |
| 不同串行单位之间 | 并行,但抢同一组 worker,可能相互拖累 | 并行,worker 隔离,完全互不影响 |
| 冷热 key 不均 | worker 池能把空闲线程让给热 key | 热 key 拖死自己那池,其它池闲着帮不上 |
| CPU 缓存 / 亲和性 | handler 在哪个 worker 跑由 asio 调度,可能跨核迁移 | 同 key 永远同线程,cache 局部性好,可做 CPU pin |
| 资源隔离 | 弱:strand 共用线程池,排队/优先级相互影响 | 强:故障/延迟被框死在单个池内 |
| 可观测性 | 主池整体的 backlog / 利用率;细粒度需自己埋点 | 每池可独立命名线程、独立 supervision、独立统计 |
| shutdown 粒度 | 只能整池 stop(),分级 quiesce 需自己写协议 |
可逐池 stop(),分级 shutdown 直接做 |
| 代码心智模型 | 需要理解 strand、dispatch vs post、bind_executor |
“一个池一个线程,各干各的活”,直观 |
| 常见 bug | strand 重入死锁、dispatch 绕过排队、误投到错的 strand |
池间循环依赖死锁;但 strand 类的坑不存在 |
| 资源开销 | 1 个 reactor、1 组 timer fd、1 组任务队列 | N 个 reactor、N 组 timer fd、N 组任务队列;N 大时不可忽视 |
| 线程数 vs 串行单位数 | 解耦:几个 worker 撑起几千个 strand | 强绑定:串行单位数 = 线程数 |
6. 性能直觉:不同负载下两者怎么表现
| 场景 | A 更好 | B 更好 |
|---|---|---|
| 任务量在所有 key 上分布均匀 | 相当 | 相当(B 更直观) |
| 少数 key 突发热点(典型:某个连接突然狂发数据) | worker 池可以挤过去帮忙 | 热点池堵塞,别人闲着 |
| 串行单位数巨大(数千 / 数万) | 几个 worker 撑得起 | 起几千个线程不现实 |
| 关键实时路径,不能被无关任务抢线程 | strand 之间共用 worker,可能优先级反转 | 独立池,完全隔离 |
| 需要细粒度可观测、单独 supervise 某条流水线 | 难以细分 | 池子 = 监控单位 |
| 需要 cache 友好 + 可 CPU pin | 调度不确定 | 线程绑死任务,天然亲和 |
| 资源紧张 + 串行单位很少 | 一份 reactor 开销 | 浪费 |
| 对"最慢那一小部分请求"敏感(尾延迟) | work stealing 能让空闲线程帮忙,最慢的请求也不会拖太久 | 热点桶里任务只能排队等自己那根线程,少数请求会被拖得明显比其他慢 |
7. 案例:一个实时遥测流水线
下面给一个综合两种模式的设计示例 —— 一个实时遥测(telemetry)数据采集流水线,需要把来自上游的事件按数据归属(shard key)写到对应分片的内存仓库,同时还要响应"控制面事件"。这个场景里同时用到了"多 io_context + 单线程"和 strand 两种思路。
7.1 写入路径:按 shard key 路由的多池架构
// 概念示意,非生产代码
class KeyShardedPool {
public:
void start() {
for (int i = 0; i <= kNumShards; ++i) {
ios_[i] = std::make_shared<boost::asio::io_context>();
guards_[i] = std::make_shared<boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>>(
boost::asio::make_work_guard(*ios_[i]));
threads_[i] = std::thread([this, i] {
pthread_setname_np(pthread_self(),
("Shard_" + std::to_string(i)).c_str());
ios_[i]->run();
});
}
}
// 按 shard key 路由:同 key 永远落同一线程
template <typename Fn>
void post(const std::string& keyBytes, Fn&& fn) {
const int shardKey = parseShardKey(keyBytes);
const int idx = (shardKey == kNoKey)
? kNumShards // 兜底桶
: shardKey % kNumShards; // 工作桶
boost::asio::post(ios_[idx]->get_executor(), std::forward<Fn>(fn));
}
// 无 key 路由:统一落兜底线程
template <typename Fn>
void post(Fn&& fn) {
boost::asio::post(ios_[kNumShards]->get_executor(),
std::forward<Fn>(fn));
}
private:
static constexpr int kNumShards = 16; // 16 个工作桶 + 1 个兜底
static constexpr int kNoKey = -1;
std::map<int, std::shared_ptr<boost::asio::io_context>> ios_;
std::map<int, std::shared_ptr<boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>>> guards_;
std::map<int, std::thread> threads_;
};
为什么这样设计:
- 数据局部性:同一 shard 的内存仓库分片由同一线程独占,cache 命中率高。
- 强故障隔离:任何一桶卡住绝不会蔓延到其它桶。
- 可监控:每个
Shard_X是独立命名的线程,top -H/perf一眼看出哪个桶在 burn CPU。 - 延迟敏感:这条路径有严格的实时窗口要求,strand 模式下 worker 池被无关任务抢线程的风险不可接受。
7.2 消息分发层:同一 io_context 上 strand + raw post 并存
同一个系统的事件分发器又用了 strand,把"必须按序处理的事件"与"互相独立可并行的数据消息"分流:
class EventDispatcher {
public:
EventDispatcher(std::shared_ptr<boost::asio::io_context> io,
/* handlers ... */)
: ioPool_(std::move(io))
, orderedStrand_(boost::asio::make_strand(*ioPool_))
{}
// 顺序事件:必须按到达顺序处理 → 投到 strand
void onOrderedEvent(EventA evt) {
boost::asio::post(orderedStrand_,
[this, evt = std::move(evt)]() mutable {
orderedHandler_->process(std::move(evt));
});
}
// 数据消息:互相独立,可以并行解析 → 直接投 io_context
void onDataMsg(EventB msg) {
boost::asio::post(ioPool_->get_executor(),
[this, msg = std::move(msg)]() mutable {
dataHandler_->process(std::move(msg));
});
}
private:
std::shared_ptr<boost::asio::io_context> ioPool_;
boost::asio::strand<boost::asio::io_context::executor_type> orderedStrand_;
std::shared_ptr<IOrderedHandler> orderedHandler_;
std::shared_ptr<IDataHandler> dataHandler_;
};
注意这里同一个 io_context 同时承载了 strand 和 raw post:strand 内串行,raw post 并行,两者跑在同一组 worker 线程上。这是 asio 的标准用法。
7.3 系统视角下两个模式如何分工
[网络订阅线程]
│ 跨线程:boost::asio::post(io.get_executor(), ...)
▼
[ioPool: 多线程 + strand 模式 A]
├─ orderedStrand ── 顺序事件按到达顺序处理
└─ raw post ── 数据消息并发解析
│ 跨线程:keyShardedPool.post(key, fn)
▼
[KeyShardedPool: 多 io_context + 单线程 模式 B]
├─ ios[0..N-1] ── 同 shard 永远在同一线程上写仓库
└─ ios[N] ── 全局收尾(无 key 路由),与 shard 桶物理隔离
│ 跨线程:loopback().defer(...)
▼
[主事件循环线程]
触发批次完成回调、生成结果文件、外部存储持久化
要点:
- **第一层(事件分发)**负载是"高并发、不可预测"的,用 strand 模式吞吐高、节约线程。
- **第二层(分片写入)**是"强实时、要 cache 局部性、要可监控"的,用多 io_context 单线程模式更可控。
- **第三层(全局收尾)**显式
defer到主线程,做需要全局一致性的工作。
8. 选型决策树
┌─────────────────────────────────────┐
│ 串行单位很多(成千上万)? │
└──────┬───────────────────┬──────────┘
│ 是 │ 否
▼ ▼
A. strand 模式 ┌──────────────────────────────┐
│ 需要强资源隔离/优先级保证? │
└──┬───────────────────┬───────┘
│ 是 │ 否
▼ ▼
B. 多 io_context 模式 ┌──────────────────────────────┐
│ 负载是否有显著热点? │
└──┬────────────────┬─────────┘
│ 是 │ 否
▼ ▼
A. strand 看个人偏好
(有 work stealing) B 更直观,A 更省资源
补几条经验:
- 关键实时路径 + 串行单位有限 + 负载稳定 → 倾向 B。
- 网络服务器 / 大量短连接 / 每连接 1 个串行单位 → A 几乎是唯一选择(几千个 io_context 不现实)。
- 混合系统:不同子模块往往采用不同模式,关键路径用 B、控制面用 A 是常见混合策略(见上一节)。
9. 常见陷阱
9.1 strand 的"重入死锁"
boost::asio::post(strand, []{
std::promise<int> p;
boost::asio::post(strand, [&p]{ p.set_value(42); });
p.get_future().wait(); // ❌ 死锁:strand 已被占用,内层 post 永远跑不了
});
修复方式是不要在 strand 上同步等同 strand 的工作,或者改用 dispatch(但要清楚 dispatch 的语义差异)。
9.2 dispatch 绕过排队
boost::asio::post(strand, []{
std::cout << "A\n";
boost::asio::dispatch(strand, []{ std::cout << "B\n"; }); // B 立刻同步执行
std::cout << "C\n";
});
// 输出顺序:A B C(不是 A C B!)
如果你以为 dispatch 总会"排队再说",这里就会出现意想不到的执行顺序。新手期建议统一用 post。
9.3 多 io_context 模式下的"池间循环依赖"
// pools[0] 的 handler:
post_by_key(0, [&]{
std::promise<int> p;
post_by_key(1, [&]{ p.set_value(42); });
p.get_future().wait(); // ❌ 风险:若 pools[1] 也在等 pools[0],死锁
});
多 io_context 模式没有 strand 重入这种陷阱,但池间互相等任务就要小心。建议:池间只做单向投递,不做同步等待。
9.4 strand 共用 worker 导致优先级反转
strand_low_priority 收到一批 100ms 的慢任务
strand_critical 收到一个实时性要求高、需要立刻处理的紧急任务
如果 worker 池只有 4 个线程,全被 strand_low_priority 的慢任务占住,strand_critical 也得排队 —— 这是 strand 模式下经典的优先级反转。要避免就得:
- 给关键路径开独立的 io_context(回到 B 模式),或
- 给每条 strand 配单独的 thread group(asio 不直接支持,要自己写),或
- 在业务层做超时与降级。
9.5 多 io_context 模式下的"热点桶"
所有写入都集中在 shardKey == 7 这一桶,7 % N 永远定位到同一个 io_context。
其它 N-1 个池的线程闲着,这一桶的 backlog 不断累积。
asio 不提供 io_context 之间的 work stealing,缓解办法:
- 增加桶数
N,降低每桶平均负载。 - 换更分散的哈希(避免
id % N在连号 ID 上的退化)。 - 把已知热点 key 单独分一桶。
- 引入背压:backlog 超阈值时丢旧数据 + 告警,而不是无界堆积。
10. 一句话各自总结
-
模式 A(单 io_context + strand):
“少量线程 + 任意多虚拟串行通道”,胜在 效率、灵活、API 标准、有 work stealing,输在 隔离弱、心智模型重、调度不确定。
适合:串行单位多、负载分布动态、追求并发吞吐的系统。典型场景:网络服务器、消息分发层。 -
模式 B(多 io_context + 每池 1 线程):
“线程数 = 关键串行单位数,各自跑各自的”,胜在 强隔离、易监控、cache 友好、心智简单,输在 没有 work stealing、资源开销大、扩展性差。
适合:实时关键路径、负载分布稳定、需要可观测和分级 supervise 的关键服务。典型场景:实时数据流水线、独立 IO 通道隔离。
实际工程里很少二选一 —— 关键路径用 B、控制面 / 消息层用 A 是最常见的混合策略。理解两者的取舍点后,再看真实代码里那些"按 key 路由的池"、“按事件类型分通道的分发器"这种"自定义线程模型”,逻辑就会清晰很多。
11. 延伸阅读
- Boost.Asio 文档:
strand概念 - Boost.Asio 文档:Executor 与
make_strand - Christopher Kohlhoff: “Talking Async” 系列 —— 作者本人讲解 asio executor 模型。
- C++ Standard Executors 提案 P0443 —— 当前 asio executor 与未来标准化方向的关系。
一句话:strand 是逻辑串行视图,多 io_context 是物理隔离单元 —— 同样能做到"按 key 串行 + 跨 key 并行",但工程属性走两条路。 看清自己系统对吞吐 / 隔离 / 可观测 / 心智简单的偏好,选型自然就清楚了。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)