4. Reactor核心组件设计
Reactor 模式的核心思想是“用少量线程处理大量连接”,但真正落地时,它并不是一个类,而是一组职责清晰、彼此协作的组件。理解这些组件的边界,比记住某个 API 更重要。本节围绕 EventLoop、Poller、Channel、Timer、EventLoopThread 和任务队列六个对象,说明它们各自负责什么、不负责什么,以及一次可读事件从内核到业务回调的完整路径。
一、六个组件的职责边界
EventLoop 是事件循环的“调度中枢”。它本身不关心 fd 是否可读,也不关心超时如何计算,它只做一件事:在一个线程内反复执行“等待事件 → 分发事件 → 执行任务队列”的循环。一个 EventLoop 必须绑定一个线程,且只能在该线程内被驱动,这是它最重要的约束。
Poller 是 I/O 多路复用的封装层,负责把 epoll、poll 或 kqueue 的差异屏蔽掉。它对外只暴露两个动作:注册/修改/删除关注的 fd 及其事件,以及等待事件就绪并返回就绪列表。Poller 不持有业务逻辑,也不直接调用回调。
Channel 是 fd 与事件处理之间的“绑定单元”。它记录一个 fd、关心的事件(可读、可写、错误等)、实际就绪的事件,以及对应的读、写、错误、关闭回调。Channel 把“哪个 fd 发生了什么”翻译成“调用哪个函数”,是 Poller 与业务代码之间的适配器。
Timer 负责定时任务。它通常以最小堆或时间轮组织定时器,向 EventLoop 提供“最近超时时间”,使 Poller 的等待不会无限阻塞。定时器到期后,回调同样在 EventLoop 所属线程执行。
EventLoopThread 是“线程 + EventLoop”的组合体。它负责创建线程、在线程启动后构造 EventLoop 并进入循环,同时向外部提供获取该 EventLoop 指针的接口,供其他线程投递任务使用。
任务队列 是跨线程通信的入口。当其他线程需要让某个 EventLoop 执行一段逻辑时,不能直接调用其内部对象,而应把函数对象放入该 EventLoop 的任务队列,并通过 eventfd 或管道唤醒它。
二、一次可读事件的完整路径
假设一个 TCP 连接收到数据,内核把对应 fd 标记为可读。此时 Poller 的 epoll_wait 返回,EventLoop 拿到就绪列表,找到该 fd 对应的 Channel,把 revents 设置为可读,然后调用 Channel 的 readCallback。readCallback 通常由 TcpConnection 注册,它执行 read 系统调用读取数据,再交给上层业务回调处理。整个过程都在 EventLoop 所属线程内完成,没有加锁,也没有线程切换。
这里有一个容易混淆的点:Channel 并不拥有 fd,它只是“引用”fd 并记录事件状态。fd 的生命周期由 TcpConnection 或 Acceptor 管理,Channel 的销毁必须发生在 fd 关闭之前,否则会出现悬空回调。因此实际实现中,Channel 通常作为 TcpConnection 的成员,随连接一起销毁。
三、跨线程任务投递与唤醒
如果业务线程需要向某个 EventLoop 投递任务,不能直接操作它的 Channel 或 Timer,而应调用该 EventLoop 的 queueInLoop。该函数把任务放入 pendingFunctors,并判断当前线程是否为 EventLoop 所属线程:如果是,任务会在本轮循环末尾直接执行;如果不是,则通过 eventfd 写入一个字节,唤醒阻塞在 epoll_wait 的 EventLoop。EventLoop 被唤醒后,先处理 I/O 事件,再执行任务队列中的全部任务。
这种设计保证了“一个 fd 的所有操作都在同一个线程内串行执行”,从而避免了锁竞争。代价是跨线程投递需要一次系统调用唤醒,因此任务粒度不宜过细,批量投递更高效。
四、定时器与超时管理
Timer 通常以最小堆组织,堆顶是最近到期的定时器。EventLoop 在每次循环开始前,向 Timer 询问“距离最近超时还有多少毫秒”,把这个值作为 epoll_wait 的超时参数。这样既不会因为等待 I/O 而错过定时任务,也不会空转消耗 CPU。定时器到期后,Timer 把到期任务放入 EventLoop 的任务队列,由 EventLoop 统一执行,保证回调线程一致性。
五、组件协作的检查清单
| 组件 | 核心职责 | 不应做的事 |
|---|---|---|
| EventLoop | 驱动循环、分发事件、执行任务队列 | 不直接读写 fd、不解析业务协议 |
| Poller | 封装 epoll/poll,返回就绪事件 | 不持有 Channel、不调用业务回调 |
| Channel | 绑定 fd 与回调,记录事件状态 | 不拥有 fd、不管理连接生命周期 |
| Timer | 组织定时任务,提供最近超时时间 | 不直接执行回调,交由 EventLoop |
| EventLoopThread | 创建线程并运行 EventLoop | 不参与具体 I/O 处理 |
| 任务队列 | 跨线程投递任务并唤醒 EventLoop | 不直接操作 Channel 或 Timer |
把这六个组件的边界划清楚,后续实现 TcpConnection、Acceptor、TcpServer 时,才能知道每一段代码应该放在哪个线程、由谁触发、在哪个回调里执行。Reactor 的复杂度并不在于单个组件有多难,而在于组件之间的调用关系必须严格遵循“同线程串行”的约束。一旦某个回调跨线程直接操作了 Channel,整个模型的稳定性就会被破坏。