高性能I/O设计模式Reactor和Proactor

昨天购买了《程序员》杂志 2007.4期,第一时间去翻阅了一遍,其中有一篇《两种高性能I/O设计模式的比较》令人眼睛一亮,这是一篇译文,偶最近在一直想认真看看这方面的文章很久了。

文章主要是讲到了系统I/O方式可分为阻塞,非阻塞同步和非阻塞异步三类,三种方式中,非阻塞异步模式的扩展性和性能最好。主要是讲了两种IO多路复用模式:Reactor和Proactor,并对它们进行了比较。

文章还介绍了为Reactor和Proactor模式构建一个通用的,统一的对外接口并是一个完全可移植的开发框架选择方案:TProactor (ACE compatible Proactor) :http://www.terabit.com.au/solutions.php。因为Linux对aio支持的不完整,所以ACE_Proactor框架在linux上的表现很差,大部分在windows上执行正常的代码,在Linux则运行异常,甚至不能编译通过。这个问题一直困扰着很大多数ACE的用户,现在好了,有一个TProactor帮助解决了在Linux不完整支持AIO的条件下,正常使用(至少是看起来正常)ACE_Proactor。

文章主要摘要:

---------->

两种I/O多路复用模式:Reactor和Proactor

 一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。
 在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。
 而在Proactor模式中,处理器--或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。
 举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。
 在Reactor中实现读
 - 注册读就绪事件和相应的事件处理器
 - 事件分离器等待事件
 - 事件到来,激活分离器,分离器调用事件对应的处理器。
 - 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
 与如下Proactor(真异步)中的读过程比较:
 - 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
 - 事件分离器等待操作完成事件
 - 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
 - 事件分离器呼唤处理器。
 - 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。
 
实践现状 
 由Douglas Schmidt等人开发的开源C++开发框架ACE,提供了大量与平台无关,支持并发的底层类(线程,互斥量等),且在高抽象层次上,提供了两组不同的类--ACE Reactor和ACE Proactor的实现。不过,虽然二者都与平台无关,提供的接口却各异。
 ACE Proactor在windows平台上具有更为优异的性能表现,因为windows在操作系统提供了高效的异步API支持(见http://msdn2.microsoft.com/en-us/library/aa365198.aspx)。
 然而,并非所有的操作系统都在系统级大力支持异步。像很多Unix系统就没做到。因此,在Unix上,选择ACE Reactor解决方案可能更好。但这样一来,为了获得最好的性能,网络应用的开发人员必须为不同的操作系统维护多份代码:windows上以ACE Proactor为基础,而Unix系统上则采用ACE Reactor解决方案。
 
改进方案
  在这部分,我们将尝试应对为Proactor和Reactor模式建立可移植框架的挑战。在改进方案中,我们将Reactor原来位于事件处理器内的read/write操作移至分离器(不妨将这个思路称为“模拟异步”),以此寻求将Reactor多路同步IO转化为模拟异步IO。以读操作为例子,改进过程如下:
  - 注册读就绪事件及其处理器,并为分离器提供数据缓冲区地址,需要读取数据量等信息。
  - 分离器等待事件(如在select()上等待)
  - 事件到来,激活分离器。分离器执行一个非阻塞读操作(它有完成这个操作所需的全部信息),最后调用对应处理器。
  - 事件处理器处理用户自定义缓冲区的数据,注册新的事件(当然同样要给出数据缓冲区地址,需要读取的数据量等信息),最后将控制权返还分离器。
  如我们所见,通过对多路IO模式功能结构的改造,可将Reactor转化为Proactor模式。改造前后,模型实际完成的工作量没有增加,只不过参与者间对工作职责稍加调换。没有工作量的改变,自然不会造成性能的削弱。对如下各步骤的比较,可以证明工作量的恒定:
  标准/典型的Reactor:
  - 步骤1:等待事件到来(Reactor负责)
  - 步骤2:将读就绪事件分发给用户定义的处理器(Reactor负责)
  - 步骤3:读数据(用户处理器负责)
  - 步骤4:处理数据(用户处理器负责)
  改进实现的模拟Proactor:
  - 步骤1:等待事件到来(Proactor负责)
  - 步骤2:得到读就绪事件,执行读数据(现在由Proactor负责)
  - 步骤3:将读完成事件分发给用户处理器(Proactor负责)
  - 步骤4:处理数据(用户处理器负责)  
  
  对于不提供异步IO API的操作系统来说,这种办法可以隐藏socket API的交互细节,从而对外暴露一个完整的异步接口。借此,我们就可以进一步构建完全可移植的,平台无关的,有通用对外接口的解决方案。
   
 上述方案已经由Terabit P/L公司(http://www.terabit.com.au/)实现为TProactor。它有两个版本:C++和JAVA的。C++版本采用ACE跨平台底层类开发,为所有平台提供了通用统一的主动式异步接口。
  Boost.Asio库,也是采取了类似的这种方案来实现统一的IO异步接口。

<-----------

最近在项目中使用了Boost.Asio类库,其就是以Proactor这种设计模式来实现,参见:Proactor(The Boost.Asio library is based on the Proactor pattern. This design note outlines the advantages and disadvantages of this approach.),其设计文档链接:http://asio.sourceforge.net/boost_asio_0_3_7/libs/asio/doc/design/index.html

First, let us examine how the Proactor design pattern is implemented in asio, without reference to platform-specific details.

Boost.Asio的Proactor模式实现图示

Proactor design pattern (adapted from [1])

当然这两I/O设计模式,也在ACE中被大量应用,这在ACE的相关书籍中都有介绍,其中在“ACE开发者”网站中有很多不错的介绍文章。

如:ACE技术论文集-第8章 前摄器(Proactor):用于为异步事件多路分离和分派处理器的对象行为模式

ACE技术论文集-第7章 ACE反应堆(Reactor)的设计和使用:用于事件多路分离的面向对象构架

ACE程序员教程-第6章 反应堆(Reactor):用于事件多路分离和分派的体系结构模式

ACE应用-第2章 JAWS:高性能Web服务器构架

 

Proactor模式在单CPU单核系统应用中有着无可比拟的优势,现在面临的问题是:在多CPU多核的系统中,它如何更好地应用多线程的优势呢???这是很值思考和实践的,也许会产生另外一种设计模式来适应发展的需要啦。


Reactor模式:

Java NIO非堵塞技术实际是采取反应器模式,或者说是观察者(observer)模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。

同步和异步区别:有无通知(是否轮询)
堵塞和非读者区别:操作结果是否等待(是否马上又返回值),只是设计方式的不同

NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,接着我们可以处理这些数据。

反应器模式与观察者模式在某些方面极为相似:当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。

一般模型

我们想象以下情形:长途客车在路途上,有人上车有人下车,但是乘客总是希望能够在客车上得到休息。

传统的做法是:每隔一段时间(或每一个站),司机或售票员对每一个乘客询问是否下车。

反应器模式做法是:汽车是乘客访问的主体(Reactor),乘客上车后,到售票员(acceptor)处登记,之后乘客便可以休息睡觉去了,当到达乘客所要到达的目的地后,售票员将其唤醒即可。



  

反应堆模式一直在发展之中,以为高效的事件多路分离和分派提供可扩展的面向对象构架。目前用于事件多路分离的OS抽象既复杂又难以使用,因而也容易出错。反应堆本质上提供一组更高级的编程抽象,简化了事件驱动的分布式应用的设计和实现。除此而外,反应堆还将若干不同种类的事件的多路分离集成到易于使用的API中。特别地,反应堆对基于定时器的事件、信号事件、基于I/O端口监控的事件和用户定义的通知进行统一地处理。

  在本章里,我们描述怎样将反应堆用于对所有这些不同的事件类型进行多路分离。


 

 

6-1 反应堆中的内部组件和外部组件的协作

 

  如图6-1所示,ACE中的反应堆与若干内部和外部组件协同工作。其基本概念是反应堆构架检测事件的发生(通过在OS事件多路分离接口上进行侦听),并发出对预登记事件处理器(event handler)对象中的方法的“回调”(callback)。该方法由应用开发者实现,其中含有应用处理此事件的特定代码。

  于是用户(也就是,应用开发者)必须:

 

创建事件处理器,以处理他所感兴趣的某事件。在反应堆上登记,通知说他有兴趣处理某事件,同时传递他想要用以处理此事件的事件处理器的指针给反应堆。

 

  随后反应堆构架将自动地:

 

在内部维护一些表,将不同的事件类型与事件处理器对象关联起来。在用户已登记的某个事件发生时,反应堆发出对处理器中相应方法的回调。

阅读更多
换一批

没有更多推荐了,返回首页