1. Reactor模式全景
要理解Reactor,先要理解它试图解决什么问题。传统阻塞式网络服务通常采用“一连接一线程”模型:主线程在 accept() 上阻塞,每接受一个客户端连接就创建一个新线程,该线程在 read() 上阻塞等待数据,处理完再回到 read() 继续等待。这个模型在连接数很少时逻辑直观、代码简单,但当并发连接达到数千甚至上万时,线程数量、栈内存、上下文切换和锁竞争会迅速压垮系统。更关键的是,绝大多数连接在任意时刻都是“空闲”的——它们没有数据可读,却各自占用一个线程在阻塞等待,这是巨大的资源浪费。
Reactor模式的核心思想是:把“等待IO就绪”这件事从业务线程中剥离出来,交给一个或少数几个事件循环统一完成。业务代码不再阻塞在某个具体连接上,而是注册“当这个连接可读时请调用我的处理函数”。操作系统提供的IO多路复用机制(Linux下的 epoll、BSD/macOS下的 kqueue、Windows下的 IOCP 或 select/poll)正是实现这一思想的基础设施:它允许一个线程同时监视成千上万个文件描述符,并在其中任意一个就绪时返回。
Reactor的三个核心角色
Reactor模式由三个职责清晰的角色构成,理解它们的边界是掌握整个模式的关键。
Reactor(反应器/事件循环):它是整个模式的中枢,持有一个多路复用器(如 epoll 实例)和一张“文件描述符 → 处理器”的注册表。Reactor循环执行三步:调用多路复用接口等待事件、取出就绪的事件集合、根据注册表把每个事件分发给对应的处理器。Reactor本身不处理业务,它只负责“监听—分发”。
Acceptor(连接接受器):它专门处理监听套接字上的“新连接到达”事件。当Reactor检测到监听fd可读时,会调用Acceptor,由它执行 accept() 得到新的连接fd,然后把这个新fd连同对应的Handler注册到Reactor中。Acceptor是连接建立阶段的入口,一旦连接建立完成,它的任务就结束了。
Handler(处理器):它绑定到某个具体的连接fd上,负责该连接上的读写事件处理。典型实现中,Handler会持有连接缓冲区、协议解析状态机和业务回调。当Reactor通知“该fd可读”时,Handler执行非阻塞 read(),把数据放入缓冲区并尝试解析;当需要发送响应时,Handler把数据写入发送缓冲区,并关注可写事件,由Reactor在fd可写时回调它继续发送。
从单Reactor单线程到主从多Reactor
Reactor模式并非只有一种形态,随着并发压力和CPU核数的增长,它演化出几种典型拓扑。
单Reactor单线程:一个线程同时承担事件监听、连接接受、读写处理和业务计算。它的优点是实现简单、无并发竞争;缺点是所有工作串行,一旦某个Handler的业务处理耗时较长(如复杂计算或磁盘IO),整个事件循环都会被阻塞,其他连接的事件无法及时响应。它适合连接数不多、处理逻辑轻量的场景,例如教学示例或内部管理端口。
单Reactor多线程:Reactor仍然只有一个,负责监听和分发事件,但把Handler中的业务处理部分交给线程池执行。这样耗时的业务逻辑不再阻塞事件循环,IO事件的分发保持流畅。需要注意的是,如果Handler的读写操作本身也在线程池中执行,就必须处理共享缓冲区的并发问题;通常的做法是让Reactor线程只做非阻塞读写,把纯计算或阻塞调用交给线程池。
主从多Reactor:这是Netty、Muduo等高性能框架采用的经典结构。主Reactor(MainReactor)只负责监听和接受新连接,它把 accept() 得到的新连接fd分配给某个从Reactor(SubReactor);每个从Reactor运行在独立线程中,各自持有一个多路复用器,负责所辖连接上的读写事件分发。连接建立后,主Reactor不再参与该连接的任何后续工作。这种结构把“接受连接”和“处理连接”彻底分离,充分利用多核CPU,同时避免了单Reactor成为瓶颈。实践中,从Reactor的数量通常设置为CPU核数或核数的两倍。
三种拓扑的对比与选型依据
| 拓扑 | 事件监听 | 连接接受 | IO读写 | 业务计算 | 典型适用场景 |
|---|---|---|---|---|---|
| 单Reactor单线程 | 1个线程 | 同线程 | 同线程 | 同线程 | 教学示例、内部工具、连接数少 |
| 单Reactor多线程 | 1个线程 | 同线程 | Reactor线程 | 线程池 | 业务计算较重、连接数中等 |
| 主从多Reactor | Main + Sub | Main线程 | Sub线程 | Sub线程或线程池 | 高并发网关、RPC框架、消息中间件 |
选型时可以从三个维度判断:连接规模、单连接处理耗时、CPU核数。如果连接数在几百以内且处理逻辑很轻,单Reactor单线程足够且最易调试;如果业务计算明显耗时(例如涉及加解密、序列化、数据库查询),应把计算移入线程池,采用单Reactor多线程;如果连接数达到数千以上、且服务器有多核,主从多Reactor几乎是必然选择,因为它让每个SubReactor在自己的线程中独立运行事件循环,连接之间的隔离性更好,单个连接的异常也不容易拖垮全局。
一个具体的连接生命周期示例
以主从多Reactor为例,假设客户端发起一次连接并发送一条请求,完整路径如下:
- MainReactor在
epoll_wait中检测到监听fd可读,调用Acceptor执行accept(),得到新连接fd(例如fd=42)。 - Acceptor按照轮询或哈希策略,把fd=42注册到某个SubReactor的epoll实例中,关注
EPOLLIN事件,并创建对应的Handler绑定该fd。 - 客户端发送数据后,SubReactor的
epoll_wait返回fd=42可读,SubReactor调用该Handler的onRead()。 - Handler执行非阻塞
read(),把数据读入接收缓冲区,驱动协议解析状态机;若一条完整请求已解析完成,则触发业务回调。 - 业务处理产生响应数据,Handler把响应写入发送缓冲区,并向SubReactor注册
EPOLLOUT关注。 - SubReactor检测到fd=42可写,回调Handler的
onWrite(),把发送缓冲区中的数据写出;写完后取消EPOLLOUT关注,回到只监听可读的状态。
这个流程中,MainReactor只参与了第1步,后续全部由SubReactor和Handler完成。这正是主从结构“接受与处理分离”的直接体现。理解这条路径,也就理解了Reactor模式在真实框架中是如何落地的。