Reactor 是什么?

Reactor 是一种处理模式。 Reactor 模式是处理并发 I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的IO事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中。

Reactor 也是一种实现机制。 Reactor 利用事件驱动机制实现,和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反,Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的事件发生,Reactor 将主动调用应用程序注册的接口,这些接口又称为 “回调函数”。用 “好莱坞原则” 来形容 Reactor 再合适不过了:不要打电话给我们,我们会打电话通知你。

为什么要使用 Reactor?

一般来说通过 I/O 复用,epoll 模式已经可以使服务器并发几十万连接的同时,维持极高 TPS,为什么还需要 Reactor 模式?原因是原生的 I/O 复用编程复杂性比较高。

一个个网络请求可能涉及到多个 I/O 请求,相比传统的单线程完整处理请求生命期的方法,I/O 复用在人的大脑思维中并不自然,因为,程序员编程中,处理请求 A 的时候,假定 A 请求必须经过多个 I/O 操作 A1-An(两次 IO 间可能间隔很长时间),每经过一次 I/O 操作,再调用 I/O 复用时,I/O 复用的调用返回里,非常可能不再有 A,而是返回了请求 B。即请求 A 会经常被请求 B 打断,处理请求 B 时,又被 C 打断。这种思维下,编程容易出错。

Reactor模式思想:分而治之+事件驱动

1)分而治之一个连接里完整的网络处理过程一般分为accept、read、decode、process、encode、send这几步。Reactor模式将每个步骤映射为一个Task,服务端线程执行的最小逻辑单元不再是一次完整的网络请求,而是Task,且采用非阻塞方式执行。
2)事件驱动每个Task对应特定网络事件。当Task准备就绪时,Reactor收到对应的网络事件通知,并将Task分发给绑定了对应网络事件的Handler执行。
3)几个角色
Reactor:负责响应事件,将事件分发给绑定了该事件的Handler处理;
Handler:事件处理器,绑定了某类事件,负责执行对应事件的Task对事件进行处理;
Acceptor:Handler的一种,绑定了connect事件。当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。

NIO Reactor模型

一、单线程Reactor

Netty入门一:Reactor模型和Netty线程模型
1)优点:不需要做并发控制,代码实现简单清晰。
2)缺点:

  • 不能利用多核CPU;
  • 一个线程需要执行处理所有的accept、read、decode、process、encode、send事件,处理成百上千的链路时性能上无法支撑

二、多线程Reactor

Netty入门一:Reactor模型和Netty线程模型

  • 有专门一个reactor线程用于监听服务端ServerSocketChannel,接收客户端的TCP连接请求;
  • 网络IO的读/写操作等由一个worker reactor线程池负责,由线程池中的NIO线程负责监听SocketChannel事件,进行消息的读取、解码、编码和发送。
  • 一个NIO线程可以同时处理N条链路,但是一个链路只注册在一个NIO线程上处理,防止发生并发操作问题。

三、主从多线程

Netty入门一:Reactor模型和Netty线程模型
在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是在极个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。
特点:
a)服务端用于接收客户端连接的不再是个1个单独的reactor线程,而是一个boss reactor线程池;
b)服务端启用多个ServerSocketChannel监听不同端口时,每个ServerSocketChannel的监听工作可以由线程池中的一个NIO线程完成。

Reactor和Proactor对比

平时接触的开源产品如Redis、ACE,事件模型都使用的Reactor模式;而同样做事件处理的Proactor,由于操作系统的原因,相关的开源产品也少;这里学习下其模型结构,重点对比下两者的异同点;

反应器Reactor

Reactor模式结构
Netty入门一:Reactor模型和Netty线程模型
Reactor包含如下角色:

  • Handle 句柄;用来标识socket连接或是打开文件;
    Synchronous Event Demultiplexer:同步事件多路分解器:由操作系统内核实现的一个函数;用于阻塞等待发生在句柄集合上的一个或多个事件;(如select/epoll;)
  • Event Handler:事件处理接口
  • Concrete Event HandlerA:实现应用程序所提供的特定事件处理逻辑;
  • Reactor:反应器,定义一个接口,实现以下功能:
    1)供应用程序注册和删除关注的事件句柄;
    2)运行事件循环;
    3)有就绪事件到来时,分发事件到之前注册的回调函数上处理;

“反应”器名字中”反应“的由来:
“反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而是由反应器分配一个具体事件处理程序,具体事件处理程序对某个指定的事件发生做出反应;这种控制逆转又称为“好莱坞法则”(不要调用我,让我来调用你)

业务流程及时序图
Netty入门一:Reactor模型和Netty线程模型
应用启动,将关注的事件handle注册到Reactor中;
调用Reactor,进入无限事件循环,等待注册的事件到来;
事件到来,select返回,Reactor将事件分发到之前注册的回调函数中处理;

主动器Proactor

Proactor模式结构
Netty入门一:Reactor模型和Netty线程模型
Proactor主动器模式包含如下角色

  • Handle 句柄;用来标识socket连接或是打开文件;
  • Asynchronous Operation Processor:异步操作处理器;负责执行异步操作,一般由操作系统内核实现;
  • Asynchronous Operation:异步操作
  • Completion Event Queue:完成事件队列;异步操作完成的结果放到队列中等待后续使用
  • Proactor:主动器;为应用程序进程提供事件循环;从完成事件队列中取出异步操作的结果,分发调用相应的后续处理逻辑;
  • Completion Handler:完成事件接口;一般是由回调函数组成的接口;
  • Concrete Completion Handler:完成事件处理逻辑;实现接口定义特定的应用处理逻辑;

业务流程及时序图
Netty入门一:Reactor模型和Netty线程模型

  1. 应用程序启动,调用异步操作处理器提供的异步操作接口函数,调用之后应用程序和异步操作处理就独立运行;应用程序可以调用新的异步操作,而其它操作可以并发进行;
  2. 应用程序启动Proactor主动器,进行无限的事件循环,等待完成事件到来;
  3. 异步操作处理器执行异步操作,完成后将结果放入到完成事件队列;
  4. 主动器从完成事件队列中取出结果,分发到相应的完成事件回调函数处理逻辑中;

对比两者的区别
主动和被动
以主动写为例:
Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑;
Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;

可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作;
Proactor直接调用异步读写操作,调用完后立刻返回;

实现
Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;

Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)

优点
Reactor实现相对简单,对于耗时短的处理场景处理高效;
操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性;
事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;
事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,

Proactor性能更高,能够处理耗时长的并发场景;

缺点
Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;

Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现;

适用场景
Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;
Proactor:异步接收和同时处理多个服务请求的事件驱动程序;

Netty线程模型

Netty入门一:Reactor模型和Netty线程模型

netty线程模型采用“服务端监听线程”和“IO线程”分离的方式,与多线程Reactor模型类似。
抽象出NioEventLoop来表示一个不断循环执行处理任务的线程,每个NioEventLoop有一个selector,用于监听绑定在其上的socket链路。

1、串行化设计避免线程竞争
netty采用串行化设计理念,从消息的读取->解码->处理->编码->发送,始终由IO线程NioEventLoop负责。整个流程不会进行线程上下文切换,数据无并发修改风险。一个NioEventLoop聚合一个多路复用器selector,因此可以处理多个客户端连接。netty只负责提供和管理“IO线程”,其他的业务线程模型由用户自己集成。时间可控的简单业务建议直接在“IO线程”上处理,复杂和时间不可控的业务建议投递到后端业务线程池中处理。
2、定时任务与时间轮
NioEventLoop中的Thread线程按照时间轮中的步骤不断循环执行:
a)在时间片Tirck内执行selector.select()轮询监听IO事件;
b)处理监听到的就绪IO事件;
c)执行任务队列taskQueue/delayTaskQueue中的非IO任务。

3、处理IO事件
1)对于boss NioEventLoop来说,轮询到的是基本上是连接事件(OP_ACCEPT):

  • a)socketChannel = ch.accept();
  • b)将socketChannel绑定到worker NioEventLoop上;
  • c)socketChannel在worker NioEventLoop上创建register0任务;d)pipeline.fireChannelReadComplete();

2)对于worker NioEventLoop来说,轮询到的基本上是IO读写事件(以OP_READ为例):

  • a)ByteBuffer.allocateDirect(capacity);
  • b)socketChannel.read(dst);c)pipeline.fireChannelRead();d)pipeline.fireChannelReadComplete();

Netty中Reactor线程和worker线程所处理的事件

1、Server端NioEventLoop处理的事件
Netty入门一:Reactor模型和Netty线程模型
2、Client端NioEventLoop处理的事件
Netty入门一:Reactor模型和Netty线程模型


参考文章:https://www.jianshu.com/p/38b56531565d
https://blog.csdn.net/a724888/article/details/80741828

相关文章: