高性能IO设计的Reactor和Proactor模式
http://wenku.baidu.com/view/625b72cba1c7aa00b52acb2d.html 在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步,同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。 一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO
同步阻塞IO: 在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式!
同步非阻塞IO: 在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
异步阻塞IO: 此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!
异步非阻塞IO: 在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。 搞清楚了以上概念以后,我们再回过头来看看,Reactor模式和Proactor模式。
(其实阻塞与非阻塞都可以理解为同步范畴下才有的概念,对于异步,就不会再去分阻塞非阻塞。对于用户进程,接到异步通知后,就直接操作进程用户态空间里的数据好了。)
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。我们分别以读操作和写操作为例来看看Reactor中的具体步骤: 读取操作: 1.应用程序注册读就绪事件和相关联的事件处理器 2.事件分离器等待事件的发生 3.当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器 4.事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理 写入操作类似于读取操作,只不过第一步注册的是写就绪事件。
下面我们来看看Proactor模式中读取操作和写入操作的过程: 读取操作: 1.应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。 2.事件分离器等待读取操作完成事件 3.在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作(异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作,操作系统扮演了重要角色),并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。 4.事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。 Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步 需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
http://www.cppblog.com/pansunyou/archive/2011/01/26/io_design_patterns.html 【翻译】两种高性能I/O设计模式(Reactor/Proactor)的比较这是05年的老文章,网上应该有人早就翻译过了,我翻译它仅仅为了学习Reactor/Proactor两种TCP服务器设计模式,顺便作翻译练习。
综述 这篇文章探讨并比较两种用于TCP服务器的高性能设计模式. 除了介绍现有的解决方案,还提出了一种更具伸缩性,只需要维护一份代码并且跨平台的解决方案(含代码示例),以及其在不同平台上的微调. 此文还比较了java,c#,c++对各自现有以及提到的解决方案的实现性能. 系统I/O可分为阻塞型,非阻塞同步型以及非阻塞异步型[1,2].阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里. 结果调用者被阻塞了,这段时间了做不了任何其它事情. 更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧,调用此函数的代码会一直僵在此处直至它所读的socket缓存中有数据到来. 相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试度看吧。比如read()操作,如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用read()者"数据还没准备好,你稍后再试". 在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。 在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。 这篇文章探讨不同的I/O利用机制并提供一种跨平台的设计模式(解决方案). 希望此文可以给于TCP高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较Java,C++各自对探讨方案的实现以及性能. 我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式I/O实在是缺少可伸缩性,性能也达不到高性能服务器的要求。 两种IO多路复用方案:Reactorand Proactor 一般情况下,I/O复用机制需要事件分享器(eventdemultiplexor [1,3]).事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊:谁的什么东西送了,快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(eventhandlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数. 涉及到事件分享器的两种模式称为:Reactorand Proactor [1].Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。 而在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有overlapped的技术),事件分离者等IOCompletion事件完成[1].这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。 举另外个例子来更好地理解Reactor与Proactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法:
下面再来看看真正意义的异步模式Proactor是如何做的:
现行做法 开源C++开发框架 ACE[1,3](DouglasSchmidt,et al.开发)提供了大量平台独立的底层并发支持类(线程、互斥量等). 同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。 尽管它们都是平台独立的单元,但他们都提供了不同的接口. ACE Proactor在MS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API.[4,5]. (这段可能过时了点吧) 不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说,许多Unix系统就有麻烦.因此,ACE Reactor可能是Unix系统上更合适的解决方案. 正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码: 为Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor. 就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为Reactor和Proactor设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。 更好的解决方案 在文章这一段时,我们将尝试提供一种融合了Proactor和Reactor两种模式的解决方案. 为了演示这个方案,我们将Reactor稍做调整,模拟成异步的Proactor模型(主要是在事件分离器里完成本该事件处理者做的实际读写工作,我们称这种方法为"模拟异步")。 下面的示例可以看看read操作是如何完成的:
我们看到,通过为分离者(也就上面的调试者)添加一些功能,可以让Reactor模式转换为Proactor模式。所有这些被执行的操作,其实是和Reactor模型应用时完全一致的。我们只是把工作打散分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细看看下面的两个过程,他们实际上完成了一样的事情: 标准的经典的Reactor模式:
模拟的Proactor模式:
在没有底层异步I/OAPI支持的操作系统,这种方法可以帮我们隐藏掉socket接口的差异(无论是性能还是其它),提供一个完全可用的统一"异步接口"。这样我们就可以开发真正平台独立的通用接口了。 TProactor 我们提出的TProactor方案已经由TerabitP/L [6]公司实现了. 它有两种实现: C++的和Java的.C++版本使用了ACE平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。 TProactor中最重要的组件要数Engine和WaitStrategy了. Engine用于维护异步操作的生命周期;而WaitStrategy用于管理并发策略.WaitStrategy和Engine一般是成对出现的,两者间提供了良好的匹配接口. Engines和等待策略被设计成高度可组合的(完整的实现列表请参照附录1)。TProactor是高度可配置的方案,通过使用异步内核API和同步Unix API(select(),poll(),/dev/poll (Solaris 5.8+),port_get (Solaris5.10),RealTime (RT) signals (Linux 2.4+),epoll (Linux 2.6),k-queue (FreeBSD)),它内部实现了三种引擎(POSIXAIO,SUN AIO and Emulated AIO)并隐藏了六类等待策略。TProactor实现了和标准的 ACE Proactor一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。 Engines和WaitStrategies可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层API,开发人员可以选择真正的异步策略,否则用户也可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。 举例来说,对于运行在SunSolaris上的HTTP服务器,如果需要支持大量的连接数,/dev/poll或者port_get()之类的引擎是比较合适的选择;如果需要高吞吐量,那使用基本select()的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准ACEReactor/Proactor模式所无法提供的(见附录2)。 在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor相较标签的ACE Reactor在Unix/Linux系统上有大约10-35%性能提升,而在Windows上差不多(测试了吞吐量及响应时间)。 性能比较(JAVA / C++ / C#). 除了C++,我们也在Java中实现了TProactor. JDK1.4中,Java仅提供了同步方法,像C中的select() [7,8].Java TProactor基于Java的非阻塞功能(java.nio包),类似于C++的TProactor使用了select()引擎. 图1、2显示了以 bits/sec为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的echo服务器:标准ACE Reactor实现(基于RedHat Linux9.0)、TProactor C++/Java实现(Microsoft Windows平台及RedHat v9.0),以及C#实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。 这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。 图1. Windows XP/P4 2.6GHz HyperThreading/512 MB RAM. 图2. Linux RedHat 2.4.20-smp/P4 2.6GHz HyperThreading/512 MB RAM. 用户代码示例 下面是TProactorJava实现的echo服务器代码框架。总的来说,开发者只需要实现两个接口:一是OpRead,提供存放读结果的缓存;二是OpWrite,提供存储待写数据的缓存区。同时,开发者需要通过回调onReadComplated()和onWriteCompleted()实现协议相关的业务代码。这些回调会在合适的时候被调用. class EchoServerProtocol implements AsynchHandler { AsynchChannel achannel = null; EchoServerProtocol( Demultiplexor m,SelectableChannel channel ) throws Exception { this.achannel = new AsynchChannel( m,this,channel ); } public void start() throws Exception { // called after construction System.out.println( Thread.currentThread().getName() + ": EchoServer protocol started" ); achannel.read( buffer); } public void onReadCompleted( OpRead opRead ) throws Exception { if ( opRead.getError() != null ) { // handle error,do clean-up if needed System.out.println( "EchoServer::readCompleted: " + opRead.getError().toString()); achannel.close(); return; } if ( opRead.getBytesCompleted () <= 0) { System.out.println("EchoServer::readCompleted: Peer closed " + opRead.getBytesCompleted(); achannel.close(); return; } ByteBuffer buffer = opRead.getBuffer(); achannel.write(buffer); } public void onWriteCompleted(OpWrite opWrite) throws Exception { // logically similar to onReadCompleted ... } } 结束语 TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录2中提到的问题都被很好地隐藏在内部实现中了。 从上面的图中我们可以看出C++仍旧是编写高性能服务器最佳选择,虽然Java已紧随其后。然而因为Java本身实现上的问题,其在Windows上表现不佳(这已经应该成为历史了吧)。 需要注意的是,以上针对Java的测试,都是以裸数据的形式测试的,未涉及到数据的处理(影响性能)。 纵观AIO在Linux上的快速发展[9],我们可以预计Linux内核API将会提供大量更加强健的异步API,如此一来以后基于此而实现的新的Engine/等待策略将能轻松地解决能用性方面的问题,并且这也能让标准ACEProactor接口受益。 附录I TProactor中实现的Engines 和 等待策略
附录II 所有同步等待策略可划分为两组:
让我们看看这两组的一些普遍的逻辑问题:
资源 [1] Douglas C.Schmidt,Stephen D. Huston "C++ Network Programming." 2002,Addison-Wesley ISBN 0-201-60464-7 [2] W. RichardStevens "UNIX Network Programming" vol. 1 and 2,1999,Prentice Hill,ISBN 0-13- 490012-X [3] Douglas C.Schmidt,Michael Stal,Hans Rohnert,Frank Buschmann "Pattern-OrientedSoftware Architecture: Patterns for Concurrent and Networked Objects,Volume2" Wiley & Sons,NY 2000 [4] INFO: SocketOverlapped I/O Versus Blocking/Non-blocking Mode. Q181611. Microsoft KnowledgeBase Articles. [5] Microsoft MSDN.I/O Completion Ports. [6] TProactor (ACEcompatible Proactor). [7] JavaDocjava.nio.channels [8] JavaDocJava.nio.channels.spi Class SelectorProvider [9] Linux AIOdevelopment 更多 Ian Barile"I/O Multiplexing & Scalable Socket Servers",2004 February,DDJ Further reading onevent handling The Adaptive CommunicationEnvironment Terabit Solutions 关于作者 Alex Libman hasbeen programming for 15 years. During the past 5 years his main area ofinterest is pattern-oriented multiplatform networked programming using C++ andJava. He is big fan and contributor of ACE. Vlad Gilbourd worksas a computer consultant,but wishes to spend more time listening jazz :) As ahobby,he started and runs www.corporatenews.com.auwebsite. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |