Table of contents
Open Table of contents
TL;DR
Proactor 模式以「操作完成通知」为核心,恰好是 Windows IOCP 的原生语义;而 Linux epoll/macOS kqueue 是 Reactor(就绪通知),可以在其上模拟 Proactor。反过来在 IOCP 上模拟 Reactor 极其别扭。Asio 选 Proactor 作为公共 API,是唯一能同时高效映射所有主流操作系统的选择。
从问题出发:2003 年的 C++ 网络编程有多痛
在 Asio 出现之前(2003 年),C++ 没有任何标准的、跨平台的异步 I/O 库。开发者面对的是:
- POSIX 系统:Berkeley sockets API +
select()/poll()手动多路复用 - Windows:完全不同的 IOCP (I/O Completion Ports) 模型
- 跨平台:每个项目自己写一层抽象,质量参差不齐
Christopher Kohlhoff 在 2003 年开始开发 Asio,2005 年 12 月 30 日被 Boost 接受。核心设计理念是 工具箱(toolkit)而非框架(framework)——用户控制事件循环、线程模型和内存管理,Asio 只提供可组合的原语。
两种异步模式:Reactor vs Proactor
理解 Asio 的设计选择,必须先理解这两种模式的本质差异。
Reactor:「告诉我什么时候准备好了,我自己来读」
应用程序 操作系统
| |
|--- 注册: 监视 fd 可读 --->|
| |
|<-- 通知: fd 可读了 -------|
| |
|--- read(fd, buf) ------->| <-- 应用程序自己执行 I/O
|<-- 返回数据 -------------|
代表 API:select()、poll()、epoll、kqueue。应用程序收到的是就绪通知(readiness notification)——「这个 socket 可以读了」,然后应用程序自己调用 read()。
Proactor:「帮我读,读完了告诉我结果」
应用程序 操作系统
| |
|--- 发起: 读 fd 到 buf --->|
| |
| (OS 在后台执行 I/O) |
| |
|<-- 通知: 读完了, N 字节 --| <-- OS 完成 I/O 并通知
代表 API:Windows IOCP。应用程序发起一个异步操作,提交缓冲区,操作系统在内核态完成数据传输,完成后通知应用程序。
关键差异对比
| 维度 | Reactor | Proactor |
|---|---|---|
| 通知内容 | 「fd 准备好了」 | 「操作完成了」 |
| I/O 执行者 | 应用程序 | 操作系统内核 |
| 缓冲区何时需要 | 收到就绪通知后 | 发起操作时就要提交 |
| 原生支持 | Linux epoll, macOS kqueue | Windows IOCP |
| API 形态 | 两步:等就绪 → 读写 | 一步:发起异步读写 |
为什么 Proactor 是唯一正确的选择
核心问题:IOCP 是原生 Proactor,无法自然地暴露为 Reactor API。
如果 Asio 选择 Reactor 作为公共 API:
- 在 Linux/macOS 上:直接映射,零开销
- 在 Windows 上:要在 IOCP 上模拟「就绪通知」,但 IOCP 根本不支持就绪通知——它只能告诉你操作完成了。要模拟 Reactor,你得发起一个 zero-byte 的异步读来探测就绪状态,然后再发起真正的读。荒谬且低效。
如果 Asio 选择 Proactor 作为公共 API:
- 在 Windows 上:直接映射到 IOCP,零开销,原生语义
- 在 Linux/macOS 上:在 Reactor 之上模拟 Proactor——收到就绪通知后,Asio 内部立即执行
read()/write(),然后把结果封装成「完成事件」投递给用户
这就是 模拟 Proactor(Simulated Proactor) 策略。模拟的代价很小(一次函数调用层级的间接),而反方向的模拟几乎不可行。
模拟 Proactor 的工作流程
以 Linux 上 async_read_some() 为例:
1. 用户调用 socket.async_read_some(buffer, handler)
2. Asio 内部向 epoll_reactor 注册 fd,监视 EPOLLIN
3. epoll_wait() 返回:fd 可读
↓
4. Asio 内部立即调用 read(fd, buffer) ← 模拟的关键步骤
↓
5. 将结果(字节数或错误码)+ handler 封装为完成事件
↓
6. 完成事件入队到 io_context 的任务队列
↓
7. io_context::run() 的线程出队并调用 handler(ec, bytes_transferred)
对用户来说,无论底层是 epoll 还是 IOCP,看到的 API 和行为完全一致。
投机执行优化
Asio 的 epoll_reactor 还实现了投机执行(speculative execution):在注册 epoll 之前,先尝试直接执行 I/O 操作。如果 socket 碰巧已经有数据(比如对端刚发了数据过来),就跳过 epoll 注册,直接完成操作。这在高吞吐场景下减少了系统调用次数。
Proactor 的代价:缓冲区必须提前提交
Proactor 的一个实际代价:缓冲区在操作发起时就必须提交,且在操作完成前不能释放。
在 Reactor 模式下,你收到「可读」通知后再分配缓冲区,buffer 的生命周期很短。但 Proactor 模式下,async_read(socket, buffer, handler) 调用时 buffer 就必须有效,直到 handler 被调用。如果有大量空闲连接挂着异步读操作,每个都占着一块缓冲区,内存消耗会更高。
这是一个真实的 tradeoff,但在实践中通常可以通过缓冲区池化来缓解。
Asio vs 其他异步 I/O 库
| 维度 | Asio | libuv | libevent | libev |
|---|---|---|---|---|
| 语言 | C++(模板密集、类型安全) | C | C | C |
| 模式 | Proactor | Reactor | Reactor | Reactor |
| 内存管理 | 非拥有式缓冲区,用户管理所有内存 | 内部管理缓冲区 | 内部管理缓冲区 | 极简内存管理 |
| Windows I/O | 原生 IOCP | 原生 IOCP | 仅 select() | 仅 select() |
| 线程模型 | 用户控制;strand 做同步 | 单线程事件循环 | 可多线程(evthread) | 单线程 |
| 设计哲学 | 工具箱(可组合原语) | 框架(预设循环) | 框架 | 框架 |
| 异步模型 | 完成令牌(回调/协程/future 统一) | 仅回调 | 仅回调 | 仅回调 |
关键区别:Asio 的缓冲区是非拥有式的(mutable_buffer 只是指针+长度),所有内存由用户管理。libuv/libevent 在内部分配和管理缓冲区。这让 Asio 在内存控制上更灵活,但也把生命周期管理的责任推给了用户。
Asio 与 C++ 标准化:为什么至今没进标准库
基于 Asio 的 Networking TS 在 2018 年发布为 ISO/IEC TS 19216:2018,但始终未能进入 C++ 标准。
根本原因是 C++ 委员会内部关于异步模型的路线之争:
| 方案 | 核心主张 | 状态 |
|---|---|---|
| Asio/Networking TS | 完成令牌 + Networking TS executor 模型,20 年生产验证 | 未进入标准 |
| P2300 std::execution (Senders/Receivers) | 惰性执行、可组合错误处理、统一网络/GPU/HPC | C++26 设计批准 |
2021 年 10 月 LEWG 投票显示两个方向均「无共识」。P2300 在 2024 年 6 月被设计批准进入 C++26,但 C++26 中没有标准网络库。最早可能要等 C++29 才会出现基于 senders/receivers 的网络库。
在此之前,Boost.Asio 和 standalone Asio 仍然是 C++ 异步网络编程的事实标准。