Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events

经典论文:《Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events》

定义

有多个输入源,有多个不同的EventHandler(RequestHandler)来处理不同的请求,Initiation Dispatcher用于管理EventHander,EventHandler首先要注册到Initiation Dispatcher中,然后Initiation Dispatcher根据输入的Event分发给注册的EventHandler;然而Initiation Dispatcher并不监听Event的到来,这个工作交给Synchronous Event Demultiplexer来处理。

补充:事件驱动概念

事件驱动模式可以将线程与连接分开,连接仅使用线程来处理特定的回调或处理线程上的事件。事件驱动的体系结构由事件生产者和事件消费者组成,生产者是事件的来源,它只知道事件已经产生,而消费者则需要知道事件发生的实体。它们可能参与处理事件,或者它们可能只是受事件的影响。

事件循环是 Reactor 模式的核心组件,它不断检查事件源的状态,处理已就绪的事件。循环通常以阻塞方式运行,直到有事件发生。

结构

image-20240828095833200

**Handle:**即操作系统中的句柄,是操作系统对资源的一种抽象,可以是打开的文件、一个连接(Socket)、Timer等。在网络编程中,一般指Socket Handle,文件描述符(fd)。将这个Handle注册到Synchronous Event Demultiplexer中,就可以它发生的事件,如READ、WRITE、CLOSE等事件。

**Synchronous Event Demultiplexer:**同步事件多路分用器,本质上是系统调用。比如linux中的select、poll、epoll等。它会一直阻塞直在handle上,直到有事件发生时才会返回。

**Initiation Dispatcher:**初始分发器,它提供了注册、删除与转发event handler的方法。当Synchronous Event Demultiplexer检测到handle上有事件发生时,便会通知initiation dispatcher调用特定的event handler的回调(handle_event())方法。

**Event Handler:事件处理器,**定义事件处理的回调方法:handle_event(),以供InitiationDispatcher回调使用。

**Concrete Event Handler:**具体的事件处理器,继承自Event Handler,在回调方法中会实现具体的业务逻辑。

处理流程

上面说明了Reactor模式中各个角色的作用,他们之间是如何交互的呢?论文以日志服务器(Logging Server)为例,详细讲解了Reactor模式的工作流程。这里总结如下:

  1. 注册Concrete Event Handler到Initiation Dispatcher中,当Initiation Dispatcher在某种类型的事件发生发生时向其通知,事件与handle关联。

  2. Initiation Dispatcher调用每个Event Handler的get_handle接口获取其绑定的Handle。

  3. Initiation Dispatcher调用handle_events开始事件处理循环。在这里,Initiation Dispatcher会将步骤2获取的所有Handle都收集起来,使用Synchronous Event Demultiplexer来等待这些Handle的事件发生。

  4. 当某个(或某几个)Handle的事件发生时,Synchronous Event Demultiplexer通知Initiation Dispatcher,select()根据发生事件的Handle找出对应的回调Handler。

  5. Initiation Dispatcher调用特定的Concrete Event Handler的回调方法(handel_event())来响应其关联的handle上发生的事件。

image-20240828095953099

优缺点

优点

  • 解耦
  • 提升复用性,且弹性设置【根据任务的不同设置不同的CPU数量】
  • 模块化【每个模块分工明确】
  • 可移植性
  • 事件驱动
  • 细力度的并发控制【和线程池相比,减少锁的粒度】【我认为是一种以时间换取CPU资源的思想】

缺点

  • Reactor增加了一定的复杂性,不易于调试。
  • Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效
  • Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。

Reactor模式 vs Proactor模式

Proactor模式结构

image-20240828101708484

Proactor主动器模式包含如下角色:

Handle 句柄;用来标识socket连接或是打开文件;

Asynchronous Operation Processor:异步操作处理器;负责执行异步操作,一般由操作系统内核实现;

Asynchronous Operation:异步操作

Completion Event Queue:完成事件队列;异步操作完成的结果放到队列中等待后续使用

Proactor:主动器;为应用程序进程提供事件循环;从完成事件队列中取出异步操作的结果,分发调用相应的后续处理逻辑;

Completion Handler:完成事件接口;一般是由回调函数组成的接口;

Concrete Completion Handler:完成事件处理逻辑;实现接口定义特定的应用处理逻辑;

Proactor模式时序图

image-20240828101741358

  1. 应用程序启动,调用异步操作处理器提供的异步操作接口函数,调用之后应用程序和异步操作处理就独立运行;应用程序可以调用新的异步操作,而其它操作可以并发进行。

  2. 应用程序启动Proactor主动器,进行无限的事件循环,等待完成事件到来。

  3. 异步操作处理器执行异步操作,完成后将结果放入到完成事件队列。

  4. 主动器从完成事件队列中取出结果,分发到相应的完成事件回调函数处理逻辑中。

区别

  • 在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。

  • 而在Proactor模式中,处理器–或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。

在Reactor中实现读:

– 注册读就绪事件和相应的事件处理器
– 事件分离器等待事件
– 事件到来,激活分离器,分离器调用事件对应的处理器。
– 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

在Proactor中实现读:

– 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
– 事件分离器等待操作完成事件
– 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
– 事件分离器呼唤处理器。
– 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。

**相同点:**都是对某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或已经完成)。在结构上,两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;

**不同点:**异步情况下(Proactor),当回调handler时,表示IO操作已经完成;同步情况下(Reactor),回调handler时,表示IO设备可以进行某个操作(可读/可写)。如果采用通俗易懂的比喻,就是:

reactor:能收了你跟俺说一声。

proactor: 你给我收十个字节,收好了跟俺说一声。

2者的优点

Reactor实现相对简单,对于耗时短的处理场景处理较高效。

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

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

2者的缺点

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

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

两者都是I/O多路复用模式,Reactor采用同步I/O,而Proactor采用异步I/O

用Reactor 模拟 Proactor的方法:

  1. 主线程往epoll内核事件表中注册socket上的可读就绪事件;
  2. 主线程调用epoll_wait等待socket上有数据可读;
  3. 当fd上有数据可读时,epoll_wait通知主线程读取数据,然后将读取到的数据插入到请求队列之中;
  4. 请求队列的空闲工作线程被唤醒,它处理完请求事件后往epoll内核事件表中注册socket的可写就绪事件;
  5. 主线程调用epoll_wait等待socket可写,当可写时,epoll_wait通知主线程,主线程往socket上写入请求的结果。

ASIO

《Boost.Asio C++ 网络编程》

最好用的Boost.Asio:现代C++网络编程

boost::io_service解读

boost.asio系列——io_service

实例解读boost库中优秀的网络库asio

观点记录:

  • 观点1:看了下boost的源码, Windows下完全就是用IOCP来实现的异步,非常简洁,因为Windows为IOCP做的事情太多了,包括建立完成事件队列,往完成队列里面添加事件等等,根本不需要额外增加其他的代码就可以完成所有的事情。
    而在linux下就没有这么方便,第一linux下的epoll是reactor模型,系统只是做简单的通知,其他拿数据的操作,还是需要自己动手,而且也不清楚是否有接口可以向epoll的队列中添加其他的事件(像Windows中的PostQueuedCompletionStatus)。 所以boost又在epoll上面加了一层,相当于自己建立了一个完成事件队列,当epoll来事件通知后boost去把数据copy到用户的buffer中,然后再把完成事件push到队列中,让用户线程去处理,这样就实现了跟IOCP相同的proactor模型。

参考资料:

深入理解Reactor模式

Redis中的Reactor模型:理解其工作机制

高性能网络编程之 Reactor 网络模型(彻底搞懂)

补充:

在网上的一些资料我看到reactor模式有单线程,多线程,主从线程模式。(比如redis就是单线程的模式)。但是论文的话只给出了一种模式范式。应该是根据这个范式自己去扩展的~