Reactor模式
也叫 Dispatcher模式
,其基于事件驱动处理模式,基本设计思想是I/O 复用结合线程池。下图为事件驱动处理示意图,可以看到大致流程为服务端程序监听处理多路请求传入的事件,并将事件分派给请求对应的处理线程完成处理
Reactor 模式中有 2 个关键组件:
Reactor
运行在一个单独的线程中,负责监听和分发事件,分发事件给适当的 Handler 来对 IO 请求做出反应Handler
将自身与事件绑定,实际执行 I/O 要完成的事件。Reactor 通过调度适当的 Handler 来响应 I/O 事件,执行非阻塞操作
根据 Reactor 的数量和处理事件的线程数量的不同,Reactor 模式有 3 种实现:
- 单 Reactor 单线程
- 单 Reactor 多线程
- 主从 Reactor 多线程
实现的示意图如下,其消息的处理流程可概括为以下几步:
- Reactor 通过 select 监控连接上的所有事件,收到事件后通过 dispatch 将其分发
- 如果该事件是请求连接建立,则由 Acceptor 接受连接,并为其创建 Handler 处理后续事件
- 假如不是建立连接事件,Reactor 将其分发给对应的 Handler 来响应
- Handler 实际处理事件,完成 read->handle->send 的业务处理流程
单Reactor单线程模型
只是进行了代码组件的区分,整体操作还是单线程,其优缺点如下:
优点
模型简单,所有处理都在一个线程中完成,没有多线程上下文切换的开销,没有进程通信及锁竞争的问题缺点
- 只有一个线程,无法完全发挥多核 CPU 的性能,造成浪费
- Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,容易导致性能瓶颈
- 一旦 Reactor 线程意外中断或者跑飞,可能导致整个系统通信模块不可用,无法接收和处理外部消息,造成节点故障
适用场景
客户端数量有限,业务处理非常快速,例如 Redis 就是使用单 Reactor 单线程模型
实现的示意图如下所示,消息处理流程可以分为以下几步:
- Reactor 通过 select 监控连接上的所有事件,收到事件后通过 dispatch 将其分发
- 如果是请求建立连接的事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 处理该连接,完成后续的读写事件
- 假如不是建立连接事件,Reactor 将其分发给对应的 Handler 来响应。该实现中 Handler 只负责响应事件,read 读取数据后会将其分发给 Worker 线程池进行 handle 业务处理
- Worker 线程池调度线程完成实际的业务处理,并将响应结果返回给主线程 Handler,Handler 通过 send 将结果返回给请求端,完成请求响应的流程
单 Reactor 多线程模型
相对于单Reactor单线程模型
来说,handle 业务逻辑交由线程池来处理,其优缺点如下:
优点
可以充分利用多核 CPU 的处理能力,使 Reactor 更专注于事件分发工作,提升整个应用的吞吐缺点
- 多线程环境下数据共享和访问比较复杂,子线程完成业务处理后把结果传递给主线程 Handler 进行发送,需要考虑共享数据的互斥和保护机制
- Reactor 主线程单线程运行,承担所有事件的监听和响应,高并发场景下会成为性能瓶颈
单 Reactor 多线程模型
中 Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,针对这个缺点一个解决方案是让 Reactor 在多线程中运行,于是产生了主从 Reactor 多线程模型
。相比第二种模型,它将 Reactor 分成两部分:
- MainReactor 只用来处理网络IO连接建立的操作,并将建立的连接指定注册到 SubReactor 上
- SubReactor 负责处理注册其上的连接的事件,完成业务处理,通常 SubReactor 个数可与CPU个数等同
主从 Reactor 多线程模型
消息处理流程可以分为以下几个步骤:
- Reactor 主线程 MainReactor 通过 select 监控建立连接的事件,收到事件后通过 Acceptor 接收,处理建立连接事件
- Acceptor 建立连接后,MainReactor 将连接分配给 Reactor 子线程 SubReactor 进行处理。SubReactor 会将该连接加入连接队列进行监听,并创建一个 Handler 用于处理该连接上的读写事件
- 当有读写事件发生时,SubReactor 调用连接对应的 Handler 进行响应,Handler 读取数据后将其分发给 Worker 线程池进行 handle 业务处理
- Worker 线程池调度线程完成实际的业务处理,并将响应结果返回给 SubReactor 的 Handler,Handler 通过 send 将结果返回给请求端,完成请求响应的流程
这种 Reactor 实现模型使用非常广泛,比较著名的包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持,其主要优点如下:
- MainReactor 与 SubReactor 的数据交互简单,MainReactor 只需把新连接交给 SubReactor,SubReactor 无需返回数据
- MainReactor 与 SubReactor 主从职责明确,MainReactor只负责接收新连接,SubReactor 负责完成后续的业务处理
在 Reactor 模式中,Reactor 在用户进程通过 select 轮询等待某个事件的发生,然后将这个事件分发给 Handler,由 Handler 来做实际的读写操作。这个过程中读写操作依然是同步的,如果把 I/O 操作改为异步,即交给操作系统来完成,就能进一步提升性能,这就是异步网络模型 Proactor
Proactor模式
也是基于事件驱动模型,不过 Proactor 不关注读取就绪事件
,而是关注读取完成事件
,因为异步IO都是操作系统将数据读写到指定的缓冲区,应用程序直接从缓冲区取用即可。以下为示意图,其各个模块组成如下:
Procator Initiator
负责创建 Procator 和 Handler,并将 Procator 和 Handler 注册到内核Asynchronous Operation Processor
负责处理注册请求,并进行 IO 操作,IO 操作完成后会通知 ProcatorProcator
Procator 在内核进程等待 IO 完成的事件,根据事件类型回调对应的 Handler 进行业务处理。Handler 负责完成实际的业务处理,也能注册新的 Handler 到内核
- 应用程序初始化 Proactor 和相应的 Handler,然后将其注册到内核
- 请求端发送数据,操作系统调用内核线程完成读取操作,并将读取的内容放入指定的缓冲区
- Proactor 在内核进程中等待读取操作完成事件,捕获到读取完成事件后,回调相应的 Handler
- Handler 直接从缓冲区读取数据处理,已经不需要进行实际的读取操作
综上可以明白 Proactor 与 Reactor 的区别:
- Reactor 由应用程序自行轮询监听事件,Proactor 则由内核进程监听事件,开销更小
- Reactor 读写的 IO 操作由应用程序自行完成处理,Proactor 是由内核异步 IO 完成读写操作,再发出通知
理论上 Proactor 比 Reactor 效率更高,但是 Proactor 有如下缺点:
内存使用
缓冲区在读写操作的时间段内必须持续占用内存,并且每个并发操作都要求有独立的缓冲区,相比 Reactor 模式在准备好读写前不要求开辟缓存,Proactor 对内存提出了更多的要求操作系统支持
异步 I/O 依赖操作系统支持,Windows 中通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统中异步 I/O 目前还不完善