跳转到正文
zeno's blog
返回

asio(一):Proactor模式-为什么Asio不用Reactor

专题: Asio

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 库。开发者面对的是:

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()epollkqueue。应用程序收到的是就绪通知(readiness notification)——「这个 socket 可以读了」,然后应用程序自己调用 read()

Proactor:「帮我读,读完了告诉我结果」

应用程序                    操作系统
   |                          |
   |--- 发起: 读 fd 到 buf --->|
   |                          |
   |    (OS 在后台执行 I/O)     |
   |                          |
   |<-- 通知: 读完了, N 字节 --|  <-- OS 完成 I/O 并通知

代表 API:Windows IOCP。应用程序发起一个异步操作,提交缓冲区,操作系统在内核态完成数据传输,完成后通知应用程序。

关键差异对比

维度ReactorProactor
通知内容「fd 准备好了」「操作完成了」
I/O 执行者应用程序操作系统内核
缓冲区何时需要收到就绪通知后发起操作时就要提交
原生支持Linux epoll, macOS kqueueWindows IOCP
API 形态两步:等就绪 → 读写一步:发起异步读写

为什么 Proactor 是唯一正确的选择

核心问题:IOCP 是原生 Proactor,无法自然地暴露为 Reactor API

如果 Asio 选择 Reactor 作为公共 API:

如果 Asio 选择 Proactor 作为公共 API:

这就是 模拟 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 库

维度Asiolibuvlibeventlibev
语言C++(模板密集、类型安全)CCC
模式ProactorReactorReactorReactor
内存管理非拥有式缓冲区,用户管理所有内存内部管理缓冲区内部管理缓冲区极简内存管理
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/HPCC++26 设计批准

2021 年 10 月 LEWG 投票显示两个方向均「无共识」。P2300 在 2024 年 6 月被设计批准进入 C++26,但 C++26 中没有标准网络库。最早可能要等 C++29 才会出现基于 senders/receivers 的网络库。

在此之前,Boost.Asio 和 standalone Asio 仍然是 C++ 异步网络编程的事实标准。


分享这篇文章:

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