前言
本文章为阅读《Linux高性能服务器编程》第八章.高性能服务器程序框架中第四小节:两种高效的事件处理模式。
Reactor模式
一、 什么是 Reactor 模式?
Reactor 翻译为“反应堆”或“反应器”。它是一种事件驱动的设计模式。
- 核心思想:主线程只负责监听(响应事件),不负责执行(业务处理)。
- 分工明确:将 I/O 事件的处理单元与逻辑处理单元分离,实现解耦。
二、 角色定位
| 角色 | 职能 (任务内容) | 备注 |
|---|---|---|
| 主线程 (I/O 处理单元) | 调用 epoll_wait 等待事件触发;将就绪的 Socket 插入请求队列。 |
不读写数据,不处理业务。 |
| 请求队列 | 存储就绪任务的缓冲区,连接 I/O 单元与逻辑单元的纽带。 | 典型的生产者-消费者模型。 |
| 工作线程 (逻辑单元) | 从队列中取出任务;读数据 (Read) -> 逻辑处理 (Process) -> 写数据 (Write) 。 | 真正干重活的地方。 |
三、 Reactor 模式的工作流程 (同步 I/O 实现版)

以图片中的典型流程为例:
- 注册与监听:主线程往
epoll事件表中注册 Socket 的读就绪事件。 - 就绪分发:当 Socket 有数据可读,
epoll_wait通知主线程,主线程将该事件放入请求队列。 - 读处理:
- 某个睡眠的工作线程被唤醒。
- 工作线程从 Socket 读取数据。
- 工作线程处理客户请求(逻辑计算)。
- 处理完后,工作线程往
epoll中注册该 Socket 的写就绪事件。
- 写处理:
- 当 Socket 可写时,
epoll_wait再次通知主线程。 - 主线程将该写事件放入请求队列。
- 工作线程被唤醒,往 Socket 写入结果。
- 当 Socket 可写时,
四、 核心设计要点
- 读写操作的位置
在 Reactor 模式中,读写数据是由工作线程完成的。
- 这一点区分了它与 Proactor 模式(Proactor 模式下,读写是由内核完成并通知程序结果的)
- 线程无差别化
如图片末尾所言: Reactor 模式中,没有必要区分专门的“读线程”和“写线程”。
- 工作线程从队列取出事件后,根据事件类型(读或写) 来决定具体的执行动作。这种设计提高了线程的复用率和系统的灵活性。
- 与
EPOLLONESHOT的联动 (进阶要点)
在多线程 Reactor 模型中:
- 当一个工作线程在处理某个请求时,主线程应通过
EPOLLONESHOT确保该 Socket 不会被其他工作线程同时抢占。 - 工作线程处理完毕并重置事件后,该 Socket 才能再次进入下一轮的 Reactor 流程。
五、 优缺点总结 - 优点:
- 高并发管理:主线程专注于 I/O 调度,能同时处理成千上万个连接。
- 响应快:避免了 I/O 阻塞导致逻辑线程闲置。
- 扩展性好:可以根据业务压力动态调整工作线程池的大小。
- 缺点:
- 主线程压力:如果 I/O 任务极其频繁(例如大量小包),主线程分发任务可能成为瓶颈。
Proactor模式
一、 什么是 Proactor 模式?
Proactor 模式是一种异步 I/O 模式。
- 核心思想:所有的 I/O 操作(读和写)都交给主线程和内核来处理。
- 职责纯粹:工作线程(逻辑单元)仅负责业务逻辑,完全不参与 I/O 读取和写入的操作。
- 实现依赖:通常需要异步 I/O 模型(如 Linux 下的
aio_read/aio_write)的支持。
二、 角色分工
| 角色 | 职能 (任务内容) |
|---|---|
| 主线程 / 内核 | 负责 I/O 监控、数据的实际读取和发送。 |
| 用户缓冲区 | 在读写前由应用程序指定,内核直接将结果放入或取出。 |
| 信号处理函数 / 回调机制 | 当内核完成 I/O 后,通知应用程序。 |
| 工作线程 (逻辑单元) | 仅从缓冲区拿到已经读好的数据进行业务计算,或者准备好数据交给内核去发。 |
三、 Proactor 模式的工作流程 (以 aio 为例)
读操作流程
- 注册与预告:主线程调用
aio_read。不仅告诉内核要盯住哪个 Socket,还告诉内核:“这是我准备好的缓冲区地址,写完了记得用信号(或回调)通知我。” - 静默执行:主线程继续处理其他事情。内核负责等待数据到达,并将数据自动拷贝到用户指定的缓冲区。
- 完成通知:数据进缓冲区后,内核发送信号。
- 业务处理:应用程序预先定义的信号处理函数(或其他机制)选择一个工作线程。工作线程直接操作缓冲区内的现成数据。
写操作流程
- 提交请求:工作线程处理完逻辑后,调用
aio_write,告诉内核缓冲区位置。 - 异步发送:主线程/内核负责将缓冲区数据发送出去。
- 后续扫尾:内核发送信号通知写操作完成。应用程序调用工作线程进行善后(如关闭连接)。
四、 Reactor vs Proactor (核心区别回顾)
| 特性 | Reactor | Proactor |
|---|---|---|
| I/O 类型 | 主要是同步 I/O(如 epoll) | 异步 I/O(如 aio) |
| 谁负责读写数据? | 工作线程 (调用 recv/send) |
内核/主线程 (完成后由内核通知) |
| 通知时机 | I/O 就绪(可以读了) | I/O 完成(已经读完了) |
| 编程难度 | 相对较低,容易实现 | 高(对异步 I/O API 的稳定性要求高) |
五、 总结
- Reactor 是“来了活,你(工作线程)快去干(读/写)”。
- Proactor 是“活干完(读完/写完)了,你(工作线程)来处理结果”。
Proactor 模式在理论上拥有更高的效率,因为它最大限度地利用了内核的异步处理能力,减少了应用层在读写过程中的等待。
同步I/O模拟Proactor
一、 核心思想
在模拟 Proactor 模式中,主线程不仅负责 I/O 监控,还亲自负责数据的读取和写入。
- 对工作线程而言:它看到的永远是“现成”的结果。它不需要调用
recv或send,只需要处理已经读到内存里的业务数据。 - 模拟的本质:主线程通过同步 I/O 完成读写,然后向工作线程发送“完成通知”,从而在逻辑上实现了 Proactor 的“异步内味儿”。
二、 角色职责划分
| 角色 | 负责的任务 |
|---|---|
| 主线程 | 1. 使用 epoll_wait 监听事件。2. 读数据:就绪后循环读取直到无数据,封装成请求包。 3. 写数据:逻辑处理完后,亲自将结果写入 Socket。 |
| 请求队列 | 存放主线程已经读好的“请求对象”,等待工作线程取走。 |
| 工作线程 | 纯逻辑处理:从队列取包 -> 逻辑运算 -> 处理结果重新交回主线程。 |
三、 详细工作流程(以 epoll 为例) |
|
![]() |
读事件处理流程
- 注册:主线程向
epoll注册 Socket 的读就绪事件。 - 监听:调用
epoll_wait等待就绪。 - 主线程读取:当数据可读时,主线程循环读取 Socket 数据,直到缓冲区空。
- 分发任务:主线程将读到的数据封装成“请求对象”,插入请求队列。
- 业务加工:工作线程被唤醒,从队列取出对象,执行业务逻辑。
写事件处理流程
- 注册写事件:工作线程处理完逻辑后,向
epoll注册该 Socket 的写就绪事件。 - 监听可写:主线程调用
epoll_wait发现 Socket 可写。 - 主线程写入:主线程亲自将处理后的结果写入 Socket,发送给客户。
- 扫尾:决定是否关闭连接或重置状态。
四、 关键点对比:模拟 Proactor vs. Reactor
| 模式 | 谁负责 read/write? |
通知内容 |
|---|---|---|
| Reactor | 工作线程 | “缓冲区有数据了,你自己来读” |
| 模拟 Proactor | 主线程 | “数据我已经帮你读好了,你直接处理” |
五、 为什么叫“模拟”?
- 真正的 Proactor:读取动作是由 操作系统内核(Kernel) 通过异步 I/O(如
io_uring)完成的。 - 模拟的 Proactor:读取动作是由 用户态的主线程 通过同步 I/O(
read/write)完成的。
六、 优缺点总结
- 优点:
- 结构清晰,工作线程负载均衡,不被 I/O 等待卡住。
- 相比真正的异步 I/O,这种方式不依赖于操作系统复杂的 AIO 支持,兼容性极强。
- 缺点:
- 主线程压力极大:主线程除了监听连接,还要负责所有 Socket 的数据拷贝,如果并发极大且包体较大,主线程可能成为性能瓶颈。
