mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2989 字
8 分钟
Reactor 与 Proactor(Reactor / Proactor)
2026-06-13

一、为什么需要 Reactor 和 Proactor#

写一个服务器,最直觉的做法是一个连接一个线程。连接少的时候没问题,但当并发上到万级(经典的 C10K 问题),线程数跟着暴涨,内存和上下文切换的开销直接吃掉系统资源。每个线程占几 MB 栈空间,一万个连接就是几十 GB 内存,而且线程在 I/O 等待时白白占着 CPU 时间片。

解决方案是事件驱动:少量线程处理大量连接。线程不阻塞在某个连接上等数据,而是监听所有连接的状态变化——谁准备好了就处理谁。这就引出了两种事件驱动的核心模式:

  • Reactor:等 I/O 就绪(readable / writable),应用自己执行 I/O 操作
  • Proactor:发起 I/O 操作后由操作系统完成,应用收到完成通知

两者的区别不在于”用不用事件循环”,而在于谁来执行实际的 I/O 操作。Reactor 告诉你”可以读了,自己去读”;Proactor 告诉你”已经读完了,数据在这里”。

二、现实类比#

餐厅类比。Reactor 像餐厅的叫号系统:你拿了号等着,广播喊”3 号,你的位子好了”——这是就绪通知。你得自己走过去坐下、自己点菜、自己取餐。Proactor 像客房服务:你打电话下单,服务员做好后端到房间——这是完成通知。你不需要知道厨房怎么运作,餐到了直接吃。

邮局类比。Reactor 像信箱的提示灯亮了:“你有信”,你得自己打开信箱取信。Proactor 像快递员敲门:“你的包裹”,信已经递到你手上。

核心差异就一个:**通知之后,活谁干?**Reactor 让你干,Proactor 替你干。

三、核心思想#

3.1 Reactor 模式#

Reactor 的核心流程:事件循环通过多路复用器(epoll / kqueue / poll)等待 I/O 就绪事件,把就绪事件分发给对应的 Handler,Handler 自己执行 I/O 操作并处理数据。

sequenceDiagram participant App as 应用 participant Loop as 事件循环 participant Demux as 多路复用器 (epoll) participant Handler as 事件处理器 App->>Loop: 启动事件循环 Loop->>Demux: epoll_wait() Demux-->>Loop: fd 可读事件 Loop->>Handler: onEvent(readable) Handler->>Handler: read(fd) → 获取数据 Handler->>Handler: 处理数据 Handler->>Loop: 返回,继续循环

关键步骤拆解:

  1. 注册兴趣:应用向多路复用器注册关心的 I/O 事件(如某个 fd 可读)
  2. 等待就绪:事件循环调用 epoll_wait,阻塞直到至少一个 fd 就绪
  3. 分发事件:多路复用器返回就绪的 fd 列表,事件循环根据注册信息找到对应的 Handler
  4. 执行处理:Handler 调用 read / write 完成实际 I/O,然后处理业务逻辑

Reactor 的 I/O 是同步非阻塞的。read 不会阻塞线程,因为此时 fd 已经就绪,数据已经在内核缓冲区里了。但如果数据量大,read 仍然需要把数据从内核空间拷贝到用户空间,这个开销是省不掉的。

3.2 Proactor 模式#

Proactor 的核心流程:应用发起异步 I/O 操作后立即返回,操作系统在后台完成 I/O,完成后把结果放入完成队列,事件循环从完成队列取出结果分发给 Completion Handler。

sequenceDiagram participant App as 应用 participant Loop as 事件循环 participant OS as 操作系统 participant CQ as 完成队列 participant CH as 完成处理器 App->>OS: aio_read(fd, buffer) OS-->>App: 立即返回 OS->>OS: 内核执行读取操作 OS->>CQ: 读取完成,放入完成事件 Loop->>CQ: GetQueuedCompletionStatus() CQ-->>Loop: 完成事件(含数据) Loop->>CH: onCompletion(data) CH->>CH: 处理数据

关键步骤拆解:

  1. 发起异步操作:应用调用 aio_read,传入 buffer 和回调,立即返回
  2. 内核执行 I/O:操作系统在后台完成数据读取,数据直接写入应用提供的 buffer
  3. 通知完成:操作系统把完成事件放入完成队列
  4. 分发处理:事件循环从完成队列取出事件,调用对应的 Completion Handler

Proactor 的 I/O 是真正异步的。应用完全不参与 I/O 执行过程,连数据拷贝都是内核代劳的。这也是 Windows IOCP 性能强悍的原因——I/O 完成路径上没有用户态/内核态的来回切换。

3.3 组件对比#

Reactor 的核心组件:

  • 事件循环(Event Loop):驱动整个分发流程,循环调用多路复用器
  • 多路复用器(Demultiplexer):epoll / kqueue / poll,负责检测 I/O 就绪状态
  • 事件处理器(Event Handler):接口,定义 onEvent 方法
  • 具体处理器(Concrete Handler):实现 onEvent,在回调中执行 I/O 和业务逻辑

Proactor 的核心组件:

  • 异步操作发起器(Async Operation Initiator):应用代码,调用 aio_read 等异步接口
  • 异步操作处理器(Async Operation Processor):操作系统内核,执行实际 I/O
  • 完成事件队列(Completion Event Queue):IOCP 的完成端口,存放已完成的 I/O 结果
  • 完成处理器(Completion Handler):接口,定义 onCompletion 方法

两者的时间复杂度都是 O(1) 的事件分发,但 Proactor 在 I/O 路径上更彻底:省掉了用户态的 read / write 系统调用开销。代价是编程模型更复杂,调试也更困难——异步操作的调用和回调在不同的上下文中执行。

四、变体与对比#

4.1 特性对比#

特性ReactorProactor
触发时机I/O 就绪(readable / writable)I/O 完成(completed)
I/O 模型同步非阻塞异步
谁执行 I/O应用自己操作系统
系统调用epoll_wait + readaio_read + GetQueuedCompletionStatus
编程复杂度相对直观回调嵌套,上下文分离
平台支持Linux epoll、macOS kqueueWindows IOCP、Linux io_uring

4.2 典型实现#

Reactor 阵营

  • Linux epoll:最主流的多路复用机制,Nginx、Redis、Node.js(Unix 下)都基于它
  • macOS kqueue:BSD 系的事件通知机制,和 epoll 思路一致
  • Nginx:经典的 Reactor 实现,worker 进程各自跑一个事件循环
  • Redis:单线程 Reactor,aeMain 循环处理所有客户端请求
  • Node.js libuv:跨平台事件循环,Unix 下用 epoll / kqueue

Proactor 阵营

  • Windows IOCP:操作系统级异步 I/O,数据库和游戏服务器广泛使用
  • Boost.Asio Proactor 模式:C++ 网络库的典范实现
  • Linux io_uring:新一代异步 I/O 接口,逐渐成为 Proactor 在 Linux 上的可靠选择

4.3 混合方案#

现实往往不是非此即彼。

Boost.Asio 在设计上采用 Proactor 模式,但在 Linux 上底层用的是 epoll——一个 Reactor 机制。它用 epoll 检测就绪事件,然后在内部发起非阻塞 I/O 操作,把结果封装成完成事件投递给 Completion Handler。本质上是用 Reactor 模拟了 Proactor 的编程接口。

Go net 包走了另一条路。runtime 内部用 epoll 做事件循环(Reactor),但暴露给开发者的是阻塞式 API——conn.Read() 看起来是阻塞的,实际上 goroutine 在等待 I/O 时会被挂起,让出 M 线程。开发者写的是”一个连接一个 goroutine”的直觉代码,底层却是事件驱动。这是用协程调度隐藏 Reactor 复杂度的经典做法。

libuv 是跨平台的典范:Windows 上用 IOCP(原生 Proactor),Unix 上用 epoll / kqueue(Reactor),对外统一成事件循环 + 回调的编程模型。Node.js 就建立在 libuv 之上。

五、多语言实现#

5.1 Go:基于 net 包的 Reactor 风格并发处理#

Go 的 net 包内部用 epoll 做事件循环,但封装成了阻塞 API。这里展示 goroutine-per-connection 的典型写法,理解底层的事件驱动本质:

package main
import (
"log"
"net"
)
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096)
for {
// 看起来是阻塞读,实际 goroutine 会在 I/O 等待时挂起
// 底层由 runtime 的 epoll 事件循环驱动
n, err := conn.Read(buf)
if err != nil {
log.Printf("连接 %s 读取结束: %v", conn.RemoteAddr(), err)
return
}
// 回显:把收到的数据原样写回
if _, err := conn.Write(buf[:n]); err != nil {
log.Printf("连接 %s 写入失败: %v", conn.RemoteAddr(), err)
return
}
}
}
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
log.Println("服务启动在 :8080")
for {
conn, err := ln.Accept()
if err != nil {
log.Printf("接受连接失败: %v", err)
continue
}
// 每个连接一个 goroutine,看似一连接一线程
// 实际由 Go 调度器在少量系统线程上复用
go handleConn(conn)
}
}

如果你想更底层地用 epoll,可以用 golang.org/x/sys/unix 直接调用系统调用:

package main
import (
"log"
"golang.org/x/sys/unix"
)
func main() {
// 创建 epoll 实例
epfd, err := unix.EpollCreate1(0)
if err != nil {
log.Fatal(err)
}
defer unix.Close(epfd)
// 创建监听 socket
lfd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM|unix.SOCK_NONBLOCK, 0)
if err != nil {
log.Fatal(err)
}
defer unix.Close(lfd)
// 注册监听 fd 的可读事件(新连接到来)
event := &unix.EpollEvent{
Events: unix.EPOLLIN,
Fd: int32(lfd),
}
if err := unix.EpollCtl(epfd, unix.EPOLL_CTL_ADD, lfd, event); err != nil {
log.Fatal(err)
}
events := make([]unix.EpollEvent, 128)
for {
// Reactor 核心:等待事件就绪
nevents, err := unix.EpollWait(epfd, events, -1)
if err != nil {
continue
}
// 分发就绪事件给对应的处理器
for i := 0; i < nevents; i++ {
ev := events[i]
if int(ev.Fd) == lfd {
// 监听 fd 就绪 → 接受新连接
nfd, _, err := unix.Accept(lfd)
if err != nil {
continue
}
// 把新连接也加进 epoll
connEvent := &unix.EpollEvent{
Events: unix.EPOLLIN,
Fd: int32(nfd),
}
unix.EpollCtl(epfd, unix.EPOLL_CTL_ADD, nfd, connEvent)
log.Printf("新连接: fd=%d", nfd)
} else {
// 普通 fd 就绪 → 读取数据
buf := make([]byte, 512)
n, err := unix.Read(int(ev.Fd), buf)
if err != nil || n == 0 {
// 连接关闭或出错,从 epoll 移除
unix.EpollCtl(epfd, unix.EPOLL_CTL_DEL, int(ev.Fd), nil)
unix.Close(int(ev.Fd))
continue
}
log.Printf("fd=%d 收到 %d 字节", ev.Fd, n)
}
}
}
}

5.2 TypeScript:EventEmitter 实现 Reactor 与 Promise 实现 Proactor#

Reactor 风格——基于就绪事件的分发:

import { EventEmitter } from "events";
// 模拟多路复用器:监听多个 fd 的就绪状态
class Demultiplexer extends EventEmitter {
private fds: Map<number, string> = new Map();
// 注册对某个 fd 的兴趣
register(fd: number, interest: string): void {
this.fds.set(fd, interest);
}
// 模拟事件就绪通知(实际由 epoll/kqueue 驱动)
notifyReady(fd: number, event: string): void {
this.emit("ready", { fd, event });
}
}
// 具体事件处理器
class ReadHandler {
onEvent(fd: number, event: string): void {
if (event === "readable") {
// Reactor 特征:收到就绪通知后,应用自己执行 I/O
const data = this.doRead(fd);
console.log(`fd=${fd} 读取到: ${data}`);
}
}
private doRead(fd: number): string {
// 模拟非阻塞读取
return `来自 fd=${fd} 的数据`;
}
}
// Reactor 事件循环
const demux = new Demultiplexer();
const handler = new ReadHandler();
demux.on("ready", ({ fd, event }) => {
handler.onEvent(fd, event);
});
demux.register(3, "readable");
demux.notifyReady(3, "readable"); // 输出: fd=3 读取到: 来自 fd=3 的数据

Proactor 风格——基于 Promise 的异步完成通知:

import { open } from "fs/promises";
// Proactor 风格:发起异步操作,等完成通知
async function proactorStyle(): Promise<void> {
// 发起异步读取 → 操作系统执行 → 完成后 Promise resolve
// 应用完全不参与 I/O 执行过程
const handle = await open("/tmp/test.txt", "r");
const buffer = Buffer.alloc(1024);
// 异步读取:内核完成数据拷贝,通过 Promise 通知完成
const { bytesRead } = await handle.read(buffer, 0, 1024, 0);
// 收到完成通知时,数据已经在 buffer 里了
console.log(`读取完成,${bytesRead} 字节: ${buffer.toString("utf8", 0, bytesRead)}`);
await handle.close();
}
// 完成处理器:对应 Proactor 的 Completion Handler
async function completionHandler(
filePath: string,
onComplete: (data: string) => void
): Promise<void> {
const handle = await open(filePath, "r");
const buffer = Buffer.alloc(4096);
const { bytesRead } = await handle.read(buffer, 0, 4096, 0);
// I/O 完成后调用回调,数据已经就绪
onComplete(buffer.toString("utf8", 0, bytesRead));
await handle.close();
}
// 使用
completionHandler("/tmp/test.txt", (data) => {
console.log(`完成处理: ${data}`);
});

TypeScript/JavaScript 的 Promise + async/await 天然契合 Proactor 模型:await 挂起当前协程,I/O 完成后恢复执行。Node.js 的 fs/promisesfetch 都是这种风格。

六、生产验证#

6.1 Nginx 事件模块(Reactor)#

Nginx 是 Reactor 模式的标杆实现。每个 worker 进程跑一个事件循环,用 epoll_wait 检测就绪事件,分发给对应模块的 handler 处理。整个请求生命周期——接受连接、读取请求、发送响应——都在同一个事件循环中完成,没有线程切换开销。

源码中的关键路径:ngx_epoll_process_events 调用 epoll_wait,返回就绪事件后遍历处理。Nginx 之所以能单机支撑数万并发,核心就在这个事件循环的高效分发。

6.2 Windows IOCP(Proactor)#

IOCP(I/O Completion Port)是 Windows 内核级异步 I/O 机制。应用创建完成端口,把 socket 句柄关联上去,发起异步 I/O 操作。内核完成 I/O 后把结果投递到完成端口,工作线程通过 GetQueuedCompletionStatus 取出结果处理。

IOCP 的精妙之处在于线程池调度:完成端口内置了并发线程数限制(NumberOfConcurrentThreads),避免过多线程同时运行导致上下文切换开销。数据库服务器(SQL Server)和高性能游戏服务器大量依赖 IOCP。

6.3 Node.js libuv 事件循环#

libuv 是 Node.js 的底层事件循环库,跨平台抽象了 Reactor 和 Proactor 的差异:Windows 上用 IOCP,Unix 上用 epoll / kqueue。对外统一提供事件循环 + 回调的编程接口。

libuv 的事件循环分阶段执行:timer → pending → idle → prepare → poll(I/O)→ check → close。开发者写的回调在对应阶段被调度执行,整个模型清晰可控。

6.4 Boost.Asio#

Boost.Asio 是 C++ 网络编程的事实标准,设计上采用 Proactor 模式。在 Windows 上原生使用 IOCP,在 Linux 上用 epoll 模拟 Proactor 语义。async_read_some 发起异步操作,操作完成后回调被投递到 io_context 的事件循环中执行。

这种”Proactor 接口 + Reactor 底层”的混合方案,让同一套代码在不同平台上都能跑出不错的性能,是跨平台抽象的经典案例。

七、小结#

Reactor 和 Proactor 解决的是同一个问题:如何用少量线程高效处理大量并发 I/O。区别在于事件通知的时机:

  • Reactor 通知”I/O 就绪”,应用自己执行 read / write。编程直观,Linux 生态主流,Nginx 和 Redis 是典型代表
  • Proactor 通知”I/O 完成”,操作系统代劳了数据拷贝。性能更极致,但编程模型复杂,Windows IOCP 和 Linux io_uring 是典型代表

选型建议:

  • Linux 服务端,Reactor + epoll 是最稳妥的选择,生态成熟,调试工具丰富
  • Windows 服务端,IOCP 是操作系统提供的最佳方案
  • 跨平台需求,用 libuv 或 Boost.Asio 做抽象层,不用自己处理平台差异
  • Go 开发者不需要直接面对这两个模式,runtime 已经封装好了,写直觉的阻塞式代码即可

不要纠结哪个模式”更好”。它们是不同操作系统 I/O 模型的自然产物。理解原理,选对工具,比站队重要得多。

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Reactor 与 Proactor(Reactor / Proactor)
https://blog.souloss.com/posts/programming/behavioral/behavioral-reactor-proactor/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时