Reactor核心是单线程事件循环,非线程池;必须耦合I/O多路复用、事件分发与回调调度,否则无法保证事件顺序和资源可见性。

Reactor核心是事件循环,不是线程池
很多人一上来就用 std::thread 包一层 epoll_wait 或 select,结果发现连接一多就卡死或漏事件——这不是 Reactor,只是个带阻塞的轮询器。真正的 Reactor 必须把 I/O 多路复用、事件分发、回调调度三者耦合进一个单线程事件循环,否则无法保证事件顺序和资源可见性。
关键判断:如果你的“Reactor”里有多个线程同时调用 read()/write() 同一个 socket,或者靠锁保护 fd 集合,那它已经偏离了 Reactor 原意。POSIX 下推荐从 epoll(Linux)或 kqueue(macOS/BSD)起步,Windows 则用 IOCP——但注意:IOCP 本质是 Proactor,硬套 Reactor 会绕弯子。
用 epoll 实现最小可行事件循环
不要一开始就封装类、抽象接口。先写出能 accept + echo 的裸循环,再往上加功能。下面这段是 Linux 下可直接编译运行的核心骨架:
EAGAIN
常见错误:
- 忘记对
accept()返回的conn调用set_nonblocking(),导致后续read()阻塞整个循环 - 用 LT 模式却没做缓冲区累积,每次只读一次就返回,丢包或吞吐骤降
-
epoll_ctl(EPOLL_CTL_MOD)误用于新 fd,应只用ADD;删除时没清空内核 event cache,导致 fd 复用后触发旧事件
回调注册与生命周期管理最容易出错
不能把 std::function 直接存进 event 结构体然后跨线程调用——Reactor 是单线程的,但回调对象(比如一个 Connection 实例)可能在别的线程被析构。一旦事件触发时对象已销毁,就是 UAF。
解决方法只有两个:
- 所有回调对象必须由 Reactor 线程独占持有,例如用
std::shared_ptr+weak_ptr配合epoll_data.ptr存储句柄,每次触发前lock()检查是否还存活 - 完全避免动态分配:把回调逻辑写成函数指针+固定大小上下文(
struct conn_ctx { int fd; char buf[4096]; size_t len; };),所有数据随连接栈分配或池化管理
性能影响明显:用 shared_ptr 每次事件要两次原子计数;而栈结构虽快,但无法支持复杂协议状态机。折中方案是对象池 + 析构队列:Reactor 线程每轮循环末尾批量清理标记为 “dead” 的连接。
别碰 std::async 和 std::promise 做异步 I/O
它们底层依赖线程池,和 Reactor 的单线程事件驱动模型冲突。例如你写 std::async(std::launch::async, [&]{ write(fd, data); });,看似“异步”,实则把系统调用扔进另一个线程,既破坏了事件顺序,又让 fd 生命周期变得不可控。
真正需要异步操作(如 DNS 解析、磁盘读)时,正确做法是:
- DNS:用
getaddrinfo_a()(GNU)或自己起pthread+getaddrinfo,结果通过 pipe 或 eventfd 回传到主循环 - 文件 I/O:Linux 下用
io_uring(需 kernel ≥5.1)替代read()/write(),它能和 epoll 共享同一个 completion queue;否则老实用线程池 + callback 注册,但回调必须 post 到 Reactor 线程执行(比如用eventfd_write唤醒 epoll)
最常被忽略的一点:Reactor 不是万能胶。HTTP/2 流多路复用、QUIC 连接迁移、TLS 握手这些 CPU 密集型任务,硬塞进事件循环只会拖垮吞吐。该 offload 就 offload,关键是控制权还在 Reactor 手里——不是扔出去不管。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











