Skip to content
cxxzsh
Go back

Reactor 与 Proactor:从 I/O 就绪到 I/O 完成

高并发网络服务需要同时处理成千上万个连接。若一个连接独占一个线程,线程栈、调度和上下文切换很快就会成为瓶颈。

解决思路是让少量线程统一等待大量 I/O。ReactorProactor 是两种经典组织方式。二者的关键区别不是“是否使用异步回调”,而是:

应用收到通知时,I/O 是“已经就绪”,还是“已经完成”?

Table of contents

Open Table of contents

先给结论

严格说,epoll 这类 API 是“就绪通知 / 多路复用”;IOCPio_uring 则围绕“操作完成”传递事件。它们都常用于构建高并发 I/O 架构。

Reactor:准备好了,你自己来读

Reactor 等待的是 就绪(readiness)事件

事件到达时,应用仍要自己调用 read() / recv()write() / send()。也就是说,通知只是在说“现在可以尝试 I/O”,数据尚未由本次用户态 read() 拷入你的 buffer。

flowchart TD
    R["Reactor 事件循环"] --> W["epoll_wait / poll 等待"]
    W --> E["fd 就绪:可读或可写"]
    E --> C["分发回调"]
    C --> IO["应用调用 read / write"]
    IO --> B["处理业务或更新发送缓冲区"]
    B --> W

典型控制流如下:

  1. 应用把 fd 的读写兴趣注册到事件循环。
  2. 事件循环在 epoll_wait() 等待。
  3. 内核报告 fd 可读或可写。
  4. Reactor 调用对应回调。
  5. 回调中由应用执行 read() / write()
  6. 应用处理数据或更新连接状态,然后进入下一轮等待。
sequenceDiagram
    participant App as 应用
    participant Reactor as Reactor
    participant Kernel as 内核

    App->>Reactor: 注册 fd 的读写兴趣
    Reactor->>Kernel: epoll_wait()
    Kernel-->>Reactor: fd 可读
    Reactor-->>App: OnReadable(fd)
    App->>Kernel: read(fd, buffer)
    Kernel-->>App: 返回已读取字节数
    App->>App: 处理数据

常见 API

平台常见就绪通知 API
POSIX / Linuxselect / poll / epoll
BSD / macOSkqueue
Windowsselect / WSAPoll / WSAEventSelect
JavaSelector(NIO)
C++ 框架libevent、libev、muduo 等

伪代码

while (running) {
  const auto events = WaitForReadyEvents(epoll_fd);

  for (const auto& event : events) {
    if (event.readable) {
      const ssize_t bytes_read = read(event.fd, read_buffer, sizeof(read_buffer));
      if (bytes_read > 0) {
        HandleData(read_buffer, bytes_read);
      }
    }

    if (event.writable) {
      const ssize_t bytes_written =
          write(event.fd, pending_data, pending_size);
      UpdatePendingWrite(event.fd, bytes_written);
    }
  }
}

这里有一个实践重点:“可读 / 可写”不等于“一次就能把所有数据读完 / 写完”。

非阻塞 socket 可能只读到一部分、只写出一部分,也可能在下一次调用时返回 EAGAIN / EWOULDBLOCK。因此 Reactor 通常要维护输入、输出缓冲区和连接状态机;使用 epoll 的边沿触发时,尤其要持续读到 EAGAIN

Proactor:读好了,数据在这

Proactor 等待的是 完成(completion)事件

应用先提交 I/O 请求;操作完成后,系统把完成事件投递回来。回调中不需要再为了“取数据”调用一次 read()

flowchart TD
    P["Proactor / 异步 I/O 调度器"] --> S["提交 async_read / async_write"]
    S --> K["内核执行 I/O"]
    K --> Q["完成队列"]
    Q --> H["分发完成回调"]
    H --> B["直接处理 buffer 和结果"]

典型控制流如下:

  1. 应用提交一个异步读或写请求,并提供 buffer 与关联状态。
  2. 系统在后台执行该 I/O。
  3. 操作结束后,系统把完成事件放进完成队列。
  4. Proactor 分发完成事件。
  5. 回调直接处理 buffer、字节数与错误码。
  6. 应用视协议状态投递下一次 I/O。
sequenceDiagram
    participant App as 应用
    participant Proactor as Proactor
    participant Kernel as 内核

    App->>Proactor: async_read(buffer)
    Proactor->>Kernel: 提交异步读请求
    Note over App: 应用继续处理其他任务
    Kernel->>Kernel: 读取数据到 buffer
    Kernel-->>Proactor: 读操作完成
    Proactor-->>App: OnReadComplete(buffer, bytes)
    App->>App: 处理 buffer

常见 API

平台 / 框架常见完成通知 API
WindowsOVERLAPPED I/O + IOCP
Linuxio_uring(提交 SQE,消费 CQE)
POSIXaio_read / aio_write(实现与适用范围受平台限制)
C++Boost.Asio 的 async_* 异步操作接口

Boost.Asio 值得单独说明:它对应用暴露的是 Proactor 风格语义——异步操作完成后才调用 handler。其底层在不同平台可以使用 IOCP、epoll 等后端;不能简单地把“Asio 非 IOCP”归为 Reactor。

伪代码

proactor.AsyncRead(socket, buffer, buffer_size,
    [&buffer](std::size_t bytes_read, ErrorCode error) {
      if (!error && bytes_read > 0) {
        HandleData(buffer, bytes_read);
      }
    });

// 完成事件循环:等待并分发已结束的操作。
proactor.Run();

Proactor 也不是“从此没有 partial I/O”。底层一次异步操作依然可能只完成部分字节;框架提供的 async_read_some 往往就是这种语义。像 Boost.Asio 的 async_read 则是组合操作,会持续发起读,直到填满请求的 buffer 或发生错误。

另外,网络异步写“完成”通常表示数据已被本机协议栈接受,而不代表对端应用已经收到

并排对比

维度ReactorProactor
等待什么就绪:readable / writable完成:read done / write done
谁发起实际 I/O应用在回调中调用 read/write应用先提交,系统异步执行
回调触发时需要继续执行 I/O可直接处理结果和 buffer
基本流程通知 → 自己读 / 写 → 处理提交 → 等完成 → 处理
主要复杂度连接状态机、读写缓冲区、partial I/Opending 操作、buffer 与对象生命周期
典型强项Linux / 跨平台就绪驱动服务Windows IOCP;完成队列驱动的异步 I/O
flowchart LR
    R1["Reactor:等就绪"] --> R2["通知:可读"] --> R3["应用 read"] --> R4["处理"]
    P1["Proactor:提交 read"] --> P2["内核执行"] --> P3["通知:读完成"] --> P4["处理"]

三个容易混淆的点

有异步回调,不代表就是 Proactor

两种模型都可以是非阻塞、事件驱动、回调式的。判断方式很简单:回调里是否还要为了本次事件调用 I/O?

// Reactor:收到“可读”通知后,应用自己读。
void OnReadable(int fd) {
  char buffer[4096];
  const ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
  Process(buffer, bytes_read);
}

// Proactor:收到“读完成”通知时,数据已写入 buffer。
void OnReadComplete(char* buffer, std::size_t bytes_read) {
  Process(buffer, bytes_read);
}

Overlapped I/O 不自动等于完整 Proactor

Windows 的 ReadFile / WriteFile 配合 OVERLAPPED 是异步 I/O 原语。若提交后立刻用 GetOverlappedResult(..., TRUE) 同步等待,调用线程仍然被阻塞。

典型 Proactor 做法是把操作关联到 IOCP,由 worker 线程用 GetQueuedCompletionStatus 消费完成包。

sequenceDiagram
    participant App as 应用
    participant Kernel as 内核
    participant IOCP as IOCP
    participant Pool as Worker 线程池

    App->>Kernel: 提交 Overlapped I/O
    Kernel->>IOCP: 投递完成包
    Pool->>IOCP: GetQueuedCompletionStatus()
    IOCP-->>Pool: bytes + OVERLAPPED + key
    Pool->>Pool: 处理结果并投递下一次 I/O

IOCP 的价值不只是“异步”:它还能把大量 I/O 的完成通知汇聚到一个队列,并以受控的并发度交给 worker 线程处理。

io_uring 更接近完成模型,但边界并不绝对

Linux 传统高并发网络服务主要使用 epoll。io_uring 提供提交队列(SQ)与完成队列(CQ):提交一个操作后,内核完成处理并生成对应的 CQE,因此它天然适合 Proactor 风格。

不过 io_uring 也支持 poll 等操作,实际框架常混合使用就绪与完成语义。与其死记标签,不如看一段代码的真实控制流:它是在等 fd ready,还是在等已提交操作的结果

与线程模型的关系

I/O 模型不等于线程模型。两者都可以单线程,也都可以多线程。

线程策略Reactor 示例Proactor 示例
单线程事件循环一个 epoll loop一个 io_uring completion loop
多事件循环muduo 的 one loop per thread可按队列或连接分片
线程池Reactor + 业务线程池IOCP + worker 线程池

Reactor 里常见的结构是 one event loop per thread:一个连接长期归属一个 loop,尽量把 I/O 状态限制在单线程内,减少锁竞争。

Windows 上,经典结构则是 IOCP + 线程池:线程等待同一个完成端口,收到完成包后处理,然后为连接投递下一次异步读或写。

flowchart TD
    I["IOCP 完成端口"] --> W1["Worker 1"]
    I --> W2["Worker 2"]
    I --> W3["Worker 3"]
    W1 --> N["投递下一次异步 I/O"]
    W2 --> N
    W3 --> N

怎么选

场景倾向
Linux 服务端、依赖成熟跨平台生态Reactor(epoll)
Windows 高并发网络、命名管道或文件 I/O 服务Proactor(IOCP)
同时支持 Windows 与 Linux用统一异步抽象屏蔽后端差异
连接少、I/O 低频、同步语义已足够阻塞 I/O + 有界线程池即可
Linux 追求更低系统调用开销或统一异步 I/O评估 io_uring,并做真实压测

没有绝对更好的模型。真正需要匹配的是:目标平台、连接规模、I/O 类型、团队对生命周期管理的掌握程度,以及你的可观测性与压测结果。

记忆口诀

延伸阅读



Next Post
RAII:为什么资源应该绑定对象生命周期