Table of contents
Open Table of contents
TL;DR
io_context 不只是一个事件循环,它是异步操作的调度器:管理完成事件队列、分发 handler 到调用 run() 的线程、与操作系统的 I/O 多路复用器集成。Boost 1.66 将 io_service 拆分为 io_context(执行上下文)+ executor(轻量句柄),是为了解耦 I/O 对象与具体的执行环境。
io_context 的本质:三个角色合一
io_context 同时扮演三个角色:
- I/O 多路复用器的持有者 —— 内部包装了平台特定的 reactor(epoll/kqueue)或 proactor(IOCP)
- 完成事件队列 —— 存储待执行的 completion handler
- 调度器 —— 将 handler 分发给调用
run()的线程
平台映射
| 平台 | io_context 内部实现 | 底层机制 |
|---|---|---|
| Linux | detail::scheduler + epoll_reactor | epoll |
| macOS/BSD | detail::scheduler + kqueue_reactor | kqueue |
| Windows | detail::win_iocp_io_context | IOCP |
| 其他 | detail::scheduler + select_reactor | select() |
在 Linux/macOS 上,detail::scheduler 维护一个 op_queue<operation> 任务队列,其中有一个特殊的 task_operation_ 代表平台的 I/O reactor。当 do_run_one 出队到这个特殊操作时,会阻塞在 reactor 的等待调用上(epoll_wait / kevent)。
io_context::run() 到底在做什么
run() 的内部循环:
┌─────────────────────────────────────────────────┐
│ run() 主循环 │
│ │
│ 1. 从任务队列出队一个 operation │
│ ├─ 是普通 handler → 直接执行 │
│ └─ 是 reactor task → 进入步骤 2 │
│ │
│ 2. 阻塞等待 I/O 事件 │
│ epoll_wait() / kevent() / GetQueuedCompletion │
│ │
│ 3. I/O 事件到达 │
│ ├─ 执行实际 I/O(模拟 Proactor,仅 Linux/macOS)│
│ ├─ 创建完成事件 │
│ └─ 将 handler 入队到任务队列 │
│ │
│ 4. 执行 handler(在当前线程) │
│ │
│ 5. 回到步骤 1,直到没有更多工作 │
└─────────────────────────────────────────────────┘
关键约束:handler 只会在调用 run()(或 run_one()、poll()、poll_one())的线程上被调用。Asio 不会创建任何隐藏线程(Windows 下 select_reactor 用于非 IOCP 操作时除外)。
多线程调用 run():隐式线程池
多个线程可以同时调用同一个 io_context::run(),形成一个隐式线程池:
asio::io_context io;
// 创建线程池
std::vector<std::thread> threads;
for (int i = 0; i < std::thread::hardware_concurrency(); ++i) {
threads.emplace_back([&io] { io.run(); });
}
// 所有线程都在竞争出队并执行 handler
// handler 可能被任意一个线程执行
线程安全保证:
run()/run_one()/poll()/poll_one():可以从多个线程并发调用 ✓stop():可以从任意线程调用 ✓restart():不能与run()并发调用 ✗- 构造/析构:不是线程安全的 ✗
run() 的完整方法族
| 方法 | 行为 | 典型用途 |
|---|---|---|
run() | 阻塞直到所有工作完成 | 主事件循环 |
run_one() | 最多执行一个 handler,无 handler 时阻塞 | 与外部事件循环集成 |
run_for(duration) | 时间限制版 run() | 定时轮询 |
run_until(time_point) | 时间点限制版 run() | 定时轮询 |
poll() | 非阻塞,仅执行已就绪的 handler | 游戏主循环集成 |
poll_one() | 非阻塞,最多执行一个已就绪 handler | 精细控制 |
stop() | 通知循环退出 | 优雅关闭 |
restart() | 重置 stopped 状态 | stop() 后重用 |
异常行为
如果一个 handler 抛出异常:
- 异常从该线程的
run()调用中传播出去 - 其他线程的
run()不受影响 io_context不会被 stop- 该线程可以重新调用
run(),不需要先调用restart()
try {
io.run();
} catch (const std::exception& e) {
// 处理异常
io.run(); // 可以直接重入,不需要 restart()
}
Work Guard:防止 run() 过早退出
run() 在没有待处理的工作时会返回。如果你的程序依赖外部事件(比如定时器到期后再投递新任务),在两个任务之间的间隙 run() 可能就退出了。
// 问题:run() 可能在没有 pending work 时立即返回
asio::io_context io;
io.run(); // 没有任何异步操作,立即返回
// 解决:work_guard 让 run() 始终认为有工作要做
auto work = asio::make_work_guard(io);
io.run(); // 阻塞,直到 work.reset() 被调用
// 关闭时
work.reset(); // 允许 run() 在当前工作完成后返回
make_work_guard 内部通过 executor 的 on_work_started()/on_work_finished() 机制(legacy executor 模型)或 outstanding_work.tracked 属性(standard executor 模型)来保持 io_context 认为还有工作。
从 io_service 到 io_context + executor:为什么要拆
旧模型(Boost 1.66 之前)
io_service 同时是执行上下文和执行器,I/O 对象直接绑定到它:
// 旧 API
asio::io_service io;
asio::ip::tcp::socket sock(io); // socket 直接绑定 io_service
sock.get_io_service(); // 返回 io_service 引用
问题:I/O 对象与特定的 io_service 紧耦合。你无法把一个 socket 的 handler 转移到另一个执行环境。
新模型(Boost 1.66+)
拆分为两层:
- 执行上下文(Execution Context):重量级对象,持有执行代码的能力。
io_context和thread_pool都继承自execution_context - 执行器(Executor):轻量级句柄,可以廉价复制,代表「在某个上下文中执行工作」的能力
// 新 API
asio::io_context io;
auto ex = io.get_executor(); // 获取执行器(轻量句柄)
asio::ip::tcp::socket sock(ex); // socket 绑定执行器,而非上下文
// 可以用任何执行器
asio::thread_pool pool(4);
asio::ip::tcp::socket sock2(pool.get_executor());
演进时间线
| 版本 | 变化 |
|---|---|
| Boost 1.66 (2017) | io_service 改名为 io_context;引入 executor 概念 |
| Boost 1.70 (2019) | socket.get_io_context() 被完全移除 |
| Boost 1.74 (2020) | I/O 对象默认关联 any_io_executor(类型擦除的执行器),彻底解耦 |
any_io_executor:类型擦除的执行器
Boost 1.74 起,I/O 对象默认使用 any_io_executor 而非具体的 io_context::executor_type:
// Boost 1.74+ 的 tcp::socket 定义
using tcp::socket = basic_stream_socket<tcp>;
// 实际上是 basic_stream_socket<tcp, any_io_executor>
any_io_executor 是类型擦除的——它可以包装任何满足 executor 要求的类型。这意味着你的 socket 代码不再依赖特定的 io_context 类型,可以接受任何执行器。
concurrency_hint:构造时的优化提示
io_context 构造函数接受一个 concurrency_hint 参数:
asio::io_context io(1); // 提示:只会有一个线程调用 run()
asio::io_context io; // 默认,不做假设
当 hint 为 1 时,内部实现可以省去某些锁操作,因为知道不会有并发的 run() 调用。这在单线程模式下有可测量的性能收益。
executor 的两种模型
Asio 内部存在两套 executor 模型的支持,这是历史包袱:
| 模型 | 核心接口 | 来源 |
|---|---|---|
| Networking TS executor(legacy) | post(), dispatch(), defer(), on_work_started(), on_work_finished() | Asio 原生 |
| Standard executor(current) | execute(), query(), require() + 属性系统 | P0443 提案 |
Asio 同时支持两者,优先使用 Standard executor 模型。system_executor 是一个特殊的执行器,代表在未指定的系统线程池上执行。
associated_executor:handler 自带执行器
每个 handler 都可以关联一个执行器(associated executor),决定 handler 在哪个执行器上被调用:
// handler 的关联执行器通过 trait 查询
asio::associated_executor_t<Handler> ex =
asio::get_associated_executor(handler);
这个机制让组合操作(composed operation)能自动传播执行器。当 async_read 内部调用 async_read_some 时,内层操作的 handler 会继承外层 handler 的关联执行器。
类似的 trait 还有:
associated_allocator:handler 使用的分配器associated_cancellation_slot(Boost 1.77+):handler 的取消插槽
这些 associator trait 是 Asio 组合性(composability)的基石——它们让元数据在异步操作链中自动传播,无需用户手动转发。