跳转到正文
zeno's blog
返回

asio(二):io_context-事件循环的核心引擎

专题: Asio

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 同时扮演三个角色:

  1. I/O 多路复用器的持有者 —— 内部包装了平台特定的 reactor(epoll/kqueue)或 proactor(IOCP)
  2. 完成事件队列 —— 存储待执行的 completion handler
  3. 调度器 —— 将 handler 分发给调用 run() 的线程

平台映射

平台io_context 内部实现底层机制
Linuxdetail::scheduler + epoll_reactorepoll
macOS/BSDdetail::scheduler + kqueue_reactorkqueue
Windowsdetail::win_iocp_io_contextIOCP
其他detail::scheduler + select_reactorselect()

在 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()阻塞直到所有工作完成主事件循环
run_one()最多执行一个 handler,无 handler 时阻塞与外部事件循环集成
run_for(duration)时间限制版 run()定时轮询
run_until(time_point)时间点限制版 run()定时轮询
poll()非阻塞,仅执行已就绪的 handler游戏主循环集成
poll_one()非阻塞,最多执行一个已就绪 handler精细控制
stop()通知循环退出优雅关闭
restart()重置 stopped 状态stop() 后重用

异常行为

如果一个 handler 抛出异常:

  1. 异常从该线程的 run() 调用中传播出去
  2. 其他线程的 run() 不受影响
  3. io_context 不会被 stop
  4. 该线程可以重新调用 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+)

拆分为两层:

// 新 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 还有:

这些 associator trait 是 Asio 组合性(composability)的基石——它们让元数据在异步操作链中自动传播,无需用户手动转发。


分享这篇文章:

上一篇
asio(三):组件拆分-strand、timer、socket、buffer如何协作
下一篇
asio(一):Proactor模式-为什么Asio不用Reactor