Reactor 模式由Reactor 线程、Handlers 处理器两大角色组成,两大角色的职责分别如下:
Reactor 线程的职责:主要负责连接建立、监听IO事件、IO事件读写以及将事件分发到Handlers 处理器。
Handlers 处理器(业务处理)的职责:非阻塞的执行业务处理逻辑。
上图描述了 Reactor 的单线程模型结构,在 Reactor 单线程模型中,所有 I/O 操作(包括连接建立、数据读写、事件分发等)、业务处理,都是由一个线程完成的。单线程模型逻辑简单,缺陷也十分明显:
一个线程支持处理的连接数非常有限,CPU 很容易打满,性能方面有明显瓶颈;
当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时;
线程在处理 I/O 事件时,Select 无法同时处理连接建立、事件分发等操作;
如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用。
在单线程 Reactor 模式中,Reactor 和 Handler 都在同一条线程中执行。这样,带来了一个问题:当其中某个 Handler 阻塞时,会导致其他所有的 Handler 都得不到执行。
在这种场景下,被阻塞的 Handler 不仅仅负责输入和输出处理的传输处理器,还包括负责新连接监听的 Acceptor 处理器,可能导致服务器无响应。这是一个非常严重的缺陷,导致单线程反应器模型在生产场景中使用得比较少。
由于单线程模型有性能方面的瓶颈,多线程模型作为解决方案就应运而生了。
Reactor 多线程模型将业务逻辑交给多个线程进行处理。除此之外,多线程模型其他的操作与单线程模型是类似的,比如连接建立、IO事件读写以及事件分发等都是由一个线程来完成。
当客户端有数据发送至服务端时,Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理。
一般的请求中,耗时最长的一般是业务处理,所以用一个线程池(worker 线程池)来处理业务操作,在性能上的提升也是非常可观的。
当然,这种模型也有明显缺点,连接建立、IO 事件读取以及事件分发完全有单线程处理;比如当某个连接通过系统调用正在读取数据,此时相对于其他事件来说,完全是阻塞状态,新连接无法处理、其他连接的 IO、查询 IO 读写以及事件分发都无法完成。
对于像 Nginx、Netty 这种对高性能、高并发要求极高的网络框架,这种模式便显得有些吃力了。因为,无法及时处理新连接、就绪的 IO 事件以及事件转发等。
主从 Reactor 模型要想解决这个问题,同样需要从我们前面介绍的几个阶段中的某一个或者多个进行优化处理。
既然是主从模式,那谁主谁从呢?哪个模块使用主从呢?
在多线程模型中,我们提到,其主要缺陷在于同一时间无法处理大量新连接、IO就绪事件;因此,将主从模式应用到这一块,就可以解决这个问题。
主从 Reactor 模式中,分为了主 Reactor 和 从 Reactor,分别处理 新建立的连接、IO读写事件/事件分发。
一来,主 Reactor 可以解决同一时间大量新连接,将其注册到从 Reactor 上进行IO事件监听处理。
二来,IO事件监听相对新连接处理更加耗时,此处我们可以考虑使用线程池来处理。这样能充分利用多核 CPU 的特性,能使更多就绪的IO事件及时处理。
简言之,主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程都有独立的 Selector 对象。MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。
在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。
muduo网络库采用了主从Reactor多线程模型。有一个main Reactor负责accept连接,已建立的连接由sub Reactor全权控制:当一个新的连接到来,main Reactor只负责把它分配给某个sub Reactor,然后不再关心该连接,所有在该连接上的操作由sub Reactor完成这也带来了编码上的简化。
Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式。
需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。