最近接触了NIO,类名涉及不少术语,查了下原来这些术语均出自Reactor/Proactor两种经典的IO设计模式。读了《两种高性能I/O设计模式的比较》(附于文末)之后,中午在麦当劳点餐的时候突然意识到I/O过程和点餐这回事儿可以很好地类比:
=====================
场景1、餐厅闲时点餐的情景:
1、客户向接待员请求下单,点套餐(可乐+薯条+汉堡)。
2、客户在柜台前等待。
3、接待员从身后的食品储物架上逐一选取套餐所含食物,完成后通知客户,“套餐齐了”。
4、客户拿东西走人。
5、轮到下一个客户向接待员发起新一轮点餐请求。
注意到,只要步骤3准备套餐所含食物的过程足够快(且架子上往往提前有储备),这样效率是很高的。
=====================
场景2、餐厅忙时点餐的情景:
1、客户们在几个柜台前排长队,每个柜台前都有一个接待员。
2、此时每个接待员只负责下单,由于下单后食物不能马上就绪,接待员会对客户说,“请您在旁边稍等”,此时客户等在柜台前,一边盯住自己的餐盘;而接待员立即给队列中下一位客户提供下单服务。
3、有几名专职的配餐员负责从身后的食品储物架上逐一选取套餐所含食物,完成后通知客户,“套餐齐了”。
5、有时候,某种食物如薯条恰好卖完了,这就需要等较长时间。配餐员会对客户说,“请您去座位上等,一会儿有薯条了,我们会通知您”。或者有些餐厅的做法是,给客户发一个号码牌。
6、客户回到座位上,把号码牌摆在显眼的地方。
7、薯条就绪后,配餐员根据号码牌分发到客户的座位上。
=====================
接下来回到程序世界:点餐的客户就是需要读取数据进行处理的handler,而食物就是待读取的数据。以柜台作为分水岭,场景1说明了,如果待读取的数据能瞬间到达、就绪,用同步读取的方式效率是最高的,用户在柜台前几乎感受不到等待。然而,互联网场景中,径由网络发送给服务器的数据往往是逐块、无序、缓慢地抵达(可乐、汉堡和薯条被逐个放在餐盘这个缓冲区里),且处理线程不得不等数据读完整了才能开始处理(用户也往往等套餐齐了才离开柜台)。那么:
l同步阻塞模式即大量客户挤在柜台前等待,且什么事儿也不做。
l同步非阻塞模式即客户们下单后到座位上等待,并不时去柜台看看套餐有否就绪。
l异步模式即客户们回到座位上等待,套餐就绪后由接待员负责通知客户来拿。
如果不能预知等待数据就绪需要多久,始终选择上述任一一种模式,肯定会是低效的。
那么在客户众多的情况下(典型的互联网场景下,一台服务器伺候众多用户),有如下折衷方案:
多个柜台同时开放,每个柜台有一名接待员(事件分离器/事件选择器,监听事件到达)负责招待一群客户。由于人太多,不能全挤在柜台前,下单后客户都回到座位上等。当某个客户套餐中的部分食物就绪,如只有薯条就绪时,接待员可以:
l立即通知客户来取走(标准Reactor模式)。客户往往要来来回回跑多次才能取得完整的套餐。
l帮客户把套餐配齐,再通知客户来取走(模拟的Proactor模式,即模拟异步)。配餐的事通常由配餐员这个角色来处理,如果接待员很清闲,客户比较少,他完全可以胜任配餐员的角色;如果客户非常多,帮一大堆客户监听食物就绪事件就会忙得够呛,配餐的事可以交给专职人员。(注意场景1和场景2中谁在负责配餐工作的微妙变化)
进一步地,如果有这样一家餐厅,拥有全自动配餐设备——意味着接待员帮用户下单后,配餐过程由机器代劳,OK,我们称之为Proactor模式。
附:《两种高性能I/O设计模式的比较》我按自己的理解改动了多处译文,有问题请大家指出。
这是05年的老文章,网上应该有人早就翻译过了,我翻译它仅仅为了学习Reactor/Proactor两种IO设计模式,顺便作翻译练习。
@H_500_404@标题:
两种高性能I/O设计模式的比较
作者:
AlexanderLibman、VladimirGilbourd
原文:
时间:
November25,2005
译者:
潘孙友2010-01-26于深圳
综述
这篇文章探讨并比较两种高性能IO设计模式.除了介绍现有的解决方案,还提出了一种更具伸缩性,只需要维护一份代码并且跨平台的解决方案(含代码示例),以及其在不同平台上的微调.此文还比较了java,c#,c++对各自现有以及提到的解决方案的实现性能.
系统I/O可分为阻塞型,非阻塞同步型以及非阻塞异步型[1,2].阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里.结果调用者被阻塞了,这段时间做不了任何其它事情.更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧,调用此函数的代码会一直僵在此处,直至它所读的socket缓存中有数据到来.
相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试试看吧。比如read()操作,如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用者:"数据还没准备好,你稍后再试".
在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。
在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。
这篇文章探讨不同的I/O利用机制并提供一种跨平台的设计模式(解决方案).希望此文可以给于TCP高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较Java,C++各自对探讨方案的实现以及性能.我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式I/O实在是缺少可伸缩性,性能也达不到高性能服务器的要求。
两种IO多路复用方案:ReactorandProactor
一般情况下,I/O复用机制需要事件分离器(eventdemultiplexor[3]).事件分离器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊:“谁的什么东西送了,快来拿吧!”开发人员在开始的时候需要在分离器那里注册感兴趣的事件,并提供相应的处理者(eventhandlers)或者是回调函数;事件分离器在适当的时候会将请求的事件分发给这些handler或者回调函数.
涉及到事件分离器的两种模式称为:ReactorandProactor[1].Reactor模式基于同步I/O,而Proactor模式则基于异步I/O.在Reactor模式中,事件分离器会等待一个就绪事件,例如当文件描述符或socket已对读写操作准备好,事件分离器就把这个事件传给事先注册的事件处理函数或回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分离器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。提供给操作系统的参数包括用于存放读写数据的用户自定义缓冲区,以及这个请求完后的回调函数等信息。事件分离器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调函数。举例来说,在Windows上,事件处理者可以提交一个异步IO操作(微软称为overlapped技术),事件分离器则等待IOCompletion事件[1]表示操作完成.这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
举另外个例子来更好地理解Reactor与Proactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法:
·某个事件处理者宣称它对某个socket上的读就绪事件很感兴趣(表示socket的缓冲区可读);
·事件分离器等着这个事件的发生;
·事件发生时会唤醒事件分离器,由分离器通知先前那个事件处理者;
·事件处理者收到消息,于是去目标socket的缓冲区上读数据了,这一次读完后,会把控制权还给调度器(dispatcher),如果数据没完(缓冲区内可能只来了部分数据),它可以再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤直到把数据读完;
下面再来看看真正意义的异步模式Proactor是如何做的:
·事件处理者提交一个异步读操作(当然,操作系统必须支持异步I/O).这个时候,事件处理者根本不关心I/O读就绪事件,它只管提交这么个请求,然后等着接收这个读操作的完成事件。
·事件分离器等着这个读事件的完成;
·当事件分离器默默等待完成事件到来的同时,操作系统会在一个内核线程上执行这个读操作,它分几次从目标socket的缓冲区上读取数据,再统一转储到用户自定义缓冲区中,最后通知事件分离器,读操作完成;
·事件分离器通知之前的事件处理者。
·事件处理者这时会发现想要读的数据已经完整地放在用户自定义缓冲区中,如有需要(目标可以无限大,需要读一点处理一点),事件处理者还可以继续提交一个新的异步读操作,再将控制权交还给事件分离器。
现行做法
开源C++开发框架ACE[3](DouglasSchmidt,etal.开发)提供了大量平台独立的底层并发支持类(线程、互斥量等).同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。尽管它们都是平台独立的单元,但他们都提供了不同的接口.
ACEProactor在MS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API.[4,255); font-size:9.5pt; font-family:Verdana">5].
(这段可能过时了点吧)不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说,许多Unix系统就有麻烦.因此,ACEReactor可能是Unix系统上更合适的解决方案.正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码:为Windows准备的ACEProactor以及为Unix系列提供的ACEReactor.
就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为Reactor和Proactor设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。
更好的解决方案
在文章这一段时,我们将尝试提供一种融合了Proactor和Reactor两种模式的解决方案.为了演示这个方案,我们将Reactor稍做调整,模拟成异步的Proactor(主要是在事件分离器里完成本该事件处理者做的分多次读写socket缓冲区的工作,我们称这种方法为"模拟异步")。下面的示例可以看看read操作是如何完成的:
·事件处理者宣称对IO事件(读就绪)感兴趣,并提供给事件分离器用于存储结果的用户自定义缓冲区地址、和数据长度等参数;
·调度器(dispatcher)等待事件(比如通过select()方法);
·当有事件到来(即可读),调度器被唤醒,它负责执行一个非阻塞的读操作(前面事件处理者已经给了足够的信息了)。读完后,它去通知事件处理者。
·事件处理者这时被知会读操作已完成,它拥有完整的原先想要获取的数据了.
我们看到,通过给分离器的I/O模块(也就上面的调度器)增加一些功能,可以让Reactor模式转换为Proactor模式。且转换后的这些操作,总工作量上其实是和Reactor模式完全一致的。我们只是把任务重新分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细看看下面的两个过程,他们实际上完成了一样的事情:
标准/经典的Reactor模式:
·步骤1)等待事件(Reactor的工作)
·步骤2)发"已经可读"事件发给事先注册的事件处理者或者回调(Reactor的工作)
·步骤3)读数据(用户定义的处理者的工作)
·步骤4)处理数据(用户定义的处理者的工作)
模拟的Proactor模式:
·步骤1)等待事件(Proactor的工作)
·步骤2)读数据(现在是Proactor的工作了)
·步骤3)把“读取完毕”事件调度(dispatch)给处理者(Proactor的工作)
·步骤4)处理数据(用户定义的处理者的工作)
在没有底层异步I/OAPI支持的操作系统,这种方法可以帮我们隐藏掉socket接口的差异,提供一个完全可用的统一的"异步接口"。这样我们就可以开发真正平台独立的通用接口了。
TProactor
我们提出的TProactor方案已经由TerabitP/L[6]公司实现了.它有两种实现:C++的和Java的.C++版本使用了ACE平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。
TProactor中最重要的组件要数Engine和WaitStrategy了.Engine用于维护异步操作的生命周期;而WaitStrategy用于管理并发策略.WaitStrategy和Engine一般是成对出现的,两者间提供了良好的匹配接口.
Engines和等待策略被设计成高度可组合的(完整的实现列表请参照附录1)。TProactor是高度可配置的方案,通过使用异步内核API和同步UnixAPI(select(),poll(),/dev/poll(Solaris5.8+),port_get(Solaris5.10),RealTime(RT)signals(Linux2.4+),epoll(Linux2.6),k-queue(FreeBSD)),它内部实现了三种引擎(POSIXAIO,SUNAIOandEmulatedAIO)并隐藏了六类等待策略。TProactor实现了和标准的ACEProactor一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。
Engines和WaitStrategies可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层API,开发人员可以选择真正的异步策略,否则用户也可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。
举例来说,对于运行在SunSolaris上的HTTP服务器,如果需要支持大量的连接数,/dev/poll或者port_get()之类的引擎是比较合适的选择;如果需要高吞吐量,那使用基本select()的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准ACEReactor/Proactor模式所无法提供的(见附录2)。
在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor相较标签的ACEReactor在Unix/Linux系统上有大约10-35%性能提升,而在Windows上差不多(测试了吞吐量及响应时间)。
性能比较(JAVA/C++/C#).
除了C++,我们也在Java中实现了TProactor.JDK1.4中,Java仅提供了同步方法,像C中的select()[7,255); font-size:9.5pt; font-family:Verdana">8].JavaTProactor基于Java的非阻塞功能(java.nio包),类似于C++的TProactor使用了select()引擎.
图1、2显示了以bits/sec为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的echo服务器:标准ACEReactor实现(基于RedHatLinux9.0)、TProactorC++/Java实现(MicrosoftWindows平台及RedHatv9.0),以及C#实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。
这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。
图1.WindowsXP/P42.6GHzHyperThreading/512MBRAM.
图2.LinuxRedHat2.4.20-smp/P42.6GHzHyperThreading/512MBRAM.
下面是TProactorJava实现的echo服务器代码框架。总的来说,开发者只需要实现两个接口:一是OpRead,提供存放读结果的缓存;二是OpWrite,提供存储待写数据的缓存区。同时,开发者需要通过回调onReadComplated()和onWriteCompleted()实现协议相关的业务代码。这些回调会在合适的时候被调用.
classEchoServerProtocolimplementsAsynchHandler
{
AsynchChannelachannel=null;
EchoServerProtocol(Demultiplexorm,SelectableChannelchannel)
throwsException
this.achannel=newAsynchChannel(m,this,channel);
}
publicvoidstart()throwsException
//calledafterconstruction
System.out.println(Thread.currentThread().getName()+
":EchoServerprotocolstarted");
achannel.read(buffer);
publicvoidonReadCompleted(OpReadopRead)throwsException
if(opRead.getError()!=null)
//handleerror,doclean-upifneeded
System.out.println("EchoServer::readCompleted:"+
opRead.getError().toString());
achannel.close();
return;
if(opRead.getBytesCompleted()<=0)
System.out.println("EchoServer::readCompleted:Peerclosed"
+opRead.getBytesCompleted();
ByteBufferbuffer=opRead.getBuffer();
achannel.write(buffer);
publicvoidonWriteCompleted(OpWriteopWrite)
//logicallysimilartoonReadCompleted
...
结束语
TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录2中提到的问题都被很好地隐藏在内部实现中了。
从上面的图中我们可以看出C++仍旧是编写高性能服务器最佳选择,虽然Java已紧随其后。然而因为Java本身实现上的问题,其在Windows上表现不佳(这已经应该成为历史了吧)。
需要注意的是,以上针对Java的测试,都是以裸数据的形式测试的,未涉及到数据的处理(影响性能)。
纵观AIO在Linux上的快速发展[9],我们可以预计Linux内核API将会提供大量更加强健的异步API,如此一来以后基于此而实现的新的Engine/等待策略将能轻松地解决能用性方面的问题,并且这也能让标准ACEProactor接口受益。
附录I
TProactor中实现的Engines和等待策略
@H_500_404@引擎类型
等待策略
操作系统
POSIX_AIO(trueasync)
aio_read()/aio_write()
aio_suspend()
WaitingforRTsignal
Callbackfunction
POSIXcomplainedUNIX(notrobust)
POSIX(notrobust)
SGIIRIX,LINUX(notrobust)
SUN_AIO(trueasync)
aio_read()/aio_write()
aio_wait()
SUN(notrobust)
EmulatedAsync
Non-blockingread()/write()
select()
poll()
/dev/poll
LinuxRTsignals
Kqueue
genericPOSIX
MostlyallPOSIXimplementations
SUN
Linux
FreeBSD
附录II
所有同步等待策略可划分为两组:
·边缘触发edge-triggered(如Linux实时信号)-socket就绪后信号只发一次
·水平触发level-triggered(如select(),poll(),/dev/poll)- 始终有效
两组的逻辑:
·边缘触发组:执行I/O操作后,事件分离器会收不到后续socket就绪事件的通知.
·水平触发组:当事件分离器侦测到某socket的就绪状态,它负责回调对应的事件处理者。但在触发回调前,应该先从监听的socket描述符列表中移除之,否则相同的事件可能被派发两次。
·显然要解决上述这些问题给开发带来了额外的复杂度,而TProactor已经帮大家处理掉。
译注:
关于边缘(下蓝色)和水平(上红色)触发,一图胜千言
边缘触发仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用边缘触发模式,需要一直read/write直到出错为止,很多人反映为什么采用边缘触发模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.看来前者适用于数据到达时多而密,后者适用于数据到达时少而稀。
[1]DouglasC.Schmidt,StephenD.Huston"C++NetworkProgramming."2002,Addison-WesleyISBN0-201-60464-7
[2]W.RichardStevens"UNIXNetworkProgramming"vol.1and2,1999,PrenticeHill,ISBN0-13-490012-X
[3]DouglasC.Schmidt,MichaelStal,HansRohnert,FrankBuschmann"Pattern-OrientedSoftwareArchitecture:PatternsforConcurrentandNetworkedObjects,Volume2"Wiley&Sons,NY2000
[4]INFO:SocketOverlappedI/OVersusBlocking/Non-blockingMode.Q181611.MicrosoftKnowledgeBaseArticles.
[5]MicrosoftMSDN.I/OCompletionPorts.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/i_o_completion_ports.asp
[6]TProactor(ACEcompatibleProactor).
www.terabit.com.au
[7]JavaDocjava.nio.channels
http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/package-summary.html
[8]JavaDocJava.nio.channels.spiClassSelectorProvider
http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/spi/SelectorProvider.html
[9]LinuxAIOdevelopment
http://lse.sourceforge.net/io/aio.html,and
http://archive.linuxsymposium.org/ols2003/Proceedings/All-Reprints/Reprint-Pulavarty-OLS2003.pdf
更多IanBarile"I/OMultiplexing&ScalableSocketServers",2004February,DDJ
Furtherreadingoneventhandling
-http://www.cs.wustl.edu/~schmidt/ACE-papers.html
TheAdaptiveCommunicationEnvironment
http://www.cs.wustl.edu/~schmidt/ACE.html
TerabitSolutions
http://terabit.com.au/solutions.php
关于作者
AlexLibmanhasbeenprogrammingfor15years.Duringthepast5yearshismainareaofinterestispattern-orientedmultiplatformnetworkedprogrammingusingC++andJava.HeisbigfanandcontributorofACE.
VladGilbourdworksasacomputerconsultant,butwishestospendmoretimelisteningjazz:)Asahobby,hestartedandrunswww.corporatenews.com.auwebsite.
原文链接:https://www.f2er.com/react/308262.html