当前位置: 首页 > Linux系统, 代码艺术 > 正文

深入理解Reactor模式

1. 前 言

在上篇译文《理解Reactor模式:线程模式和事件驱动模式》 中,作者讲解了多线程模式和事件驱动模式的并发网络编程,并且基于事件驱动模型,引出了Reactor这一I/O多路复用模式。但是随着笔者对Reactor模式的深入研究,发现其设计理念在许多开源的高并发网络库中都有应用。并且还有一个于是类似的Proactor模式。本文将从Reactor模式的提出者 Douglas Schmidt 的经典论文《Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events》出发,简述其设计理念以及应用。

2. 什么是Reactor模式

2.1 Reactor模式的定义

首先,还是引用上述论文中对Reactor模式的定义:“The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. Also known as Dispatcher, Notifier”。

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

2.2 Reactor模式的结构

上面涉及到了多个名词,根据论文中的定义,Reactor模式由5个角色组成。各模块的名称和他们之间的关系可用下图表示:

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,在回调方法中会实现具体的业务逻辑。

2.3 Reactor模式的处理流程

上面说明了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上发生的事件。

详细的时序图如下:

2.4 为什么使用Reactor模式

在上文《理解Reactor模式:线程模式和事件驱动模式》 中,作者首先提出的多线程模式(Thread-Based Architecture)。那么相对与Reactor模式,他们有什么区别呢,又各有什么不同呢?上面这篇论文在开头就提出了这种模式(Thread-Per-Connection)存在三个方面的不足之处:

1. 效率:由于上下文切换,同步和数据移动,线程可能导致性能不佳;

2. 编程简洁性:线程可能需要复杂的并发控制方案;

3. 可移植性:线程并不是在所有操作系统平台都提供。

相对而言,Reactor模式又有什么优点呢。论文总结了多方面的优点:解耦、提升复用性、模块化、可移植性、事件驱动、细力度的并发控制等,这里就不一一详细赘述。那么它有没有缺点呢?也是有的,论文最后总结了Reactor模式的几点不足:

1. 相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。

2. Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。

3. Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。

3. Reactor模式 vs Proactor模式

讲到Reactor模式,熟悉事件驱动模式编程的同学们可能就会想到另一种事件驱动的I/O多路复用模式:Proactor模式。那么两者有何异同呢?我们还是再次鼻祖Douglas来了解一下Proactor模式。没错,继Reactor模式论文提出之后,他又发表了《An Object Behavioral Pattern for Demultiplexing and Dispatching Handlers for Asynchronous Events》,大述Proactor模式。这里仅简单描述其设计模式。更详细的了解,可参阅上述论文。

3.1 Proactor模式结构

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

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

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

Asynchronous Operation:异步操作

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

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

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

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

3.2 Proactor模式时序图

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

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

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

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

3.3 两者的区别

一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。

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

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

下面以读操作为例,来理解Reactor与Proactor二者的差异:
在Reactor中实现读:

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

在Proactor中实现读:

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

相同点:都是对某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或已经完成)。在结构上,两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;
不同点:异步情况下(Proactor),当回调handler时,表示IO操作已经完成;同步情况下(Reactor),回调handler时,表示IO设备可以进行某个操作(可读/可写)。如果采用通俗易懂的比喻,就是:
reactor:能收了你跟俺说一声。
proactor: 你给我收十个字节,收好了跟俺说一声。

3.4 两者的优点

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

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

3.5 两者的缺点

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

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

3.6 适用场景

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

3.7 扩展思考

上面详细讲解了Reactor模式与Proactor模式各自的特点以及内部异同。简单总结就是:两者都是I/O多路复用模式,Reactor采用同步I/O,而Proactor采用异步I/O。那么,同步I/O也可以模拟Proactor模式吗?答案是可以的。《Comparing Two High-Performance I/O Design Patterns》一文中提到了一个用Reactor 模拟 Proactor 而不借助操作系统异步机制的方法。 相对来说,Proactor模式的不同之处就是将事件通知交个操作系统了,并且在产生事件后存入用户的缓冲区(类似于消息队列),再由事件处理程序处理。那么,只要在Reactor模式中,将产出的事件插入到队列中,在由队列处理线程处理这些事件不就可以做到了。根据笔者对上面论文的理解,以读/写事件为例,说说这种模拟的大致方法:

1. 主线程往epoll内核事件表中注册socket上的可读就绪事件;

2. 主线程调用epoll_wait等待socket上有数据可读;

3. 当fd上有数据可读时,epoll_wait通知主线程读取数据,然后将读取到的数据插入到请求队列之中;

4. 请求队列的空闲工作线程被唤醒,它处理完请求事件后往epoll内核事件表中注册socket的可写就绪事件;

5. 主线程调用epoll_wait等待socket可写,当可写时,epoll_wait通知主线程,主线程往socket上写入请求的结果。

4. Reactor模式的应用

关于Reactor模式的应用,业界已经开源了不少网络库,这里将不详细分分析。仅此列出一些比较知名的应用。如:Java中的NIO与Netty,事件驱动库libevent/libev/libuv、Redis的网络模型等。采用Proactor模式的有:IOCP、Boost.Asio等。

当然, 前辈Douglas Schmidt在提出各种设计模式后,也开发出了现代面向对象网络编程的重量级的 C++ 网络库:ACE。源于什么是ACE,可以参考陈硕记述文章:《学之者生,用之者死——ACE历史与简评》。在ACE中给出了各种设计模式的具体实现,其理论方面可以参考Douglas的相关论文或者《Pattern Languages of Program Design》一书。

当然,笔者在实战项目开发过程中,也采用了基于Reactor模式的网络库来开发高并发网络程序。笔者使用的是采用GO语言开发的evio(https://github.com/tidwall/evio)并发库,关于这个并发库的详细分析与使用,可以继续关注后面的文章。

 

参考资料:

Reactor模式论文: http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf

Proactor模式论文:http://www.dre.vanderbilt.edu/~schmidt/PDF/proactor.pdf

Reactor模式详解: http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html

Redis与Reactor模式:https://blog.csdn.net/u014433009/article/details/50502028

Reator模式论文阅读整理: https://blog.csdn.net/x_i_y_u_e/article/details/52301928

IO设计模式:Reactor和Proactor对比:https://segmentfault.com/a/1190000002715832

网络编程:ReactorProactor的概念:http://www.cnblogs.com/dawen/archive/2011/05/18/2050358.html

 



这篇博文由 s0nnet 于2018年08月02日发表在 Linux系统, 代码艺术 分类下, 通告目前不可用,你可以至底部留下评论。
如无特别说明,独木の白帆发表的文章均为原创,欢迎大家转载,转载请注明: 深入理解Reactor模式 | 独木の白帆
关键字:

深入理解Reactor模式:等您坐沙发呢!

发表评论

快捷键:Ctrl+Enter