Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将全部要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。react
Reactor是一种事件驱动机制,和普通函数调用的不一样之处在于:应用程序不是主动的调用某个API完成处理,而是偏偏相反,Reactor逆置了事件处理流程,应用程序须要提供相应的接口并注册到Reactor上,若是相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。用“好莱坞原则”来形容Reactor再合适不过了:不要打电话给咱们,咱们会打电话通知你。linux
Reactor模式与Observer模式在某些方面极为类似:当一个主体发生改变时,全部依属体都获得通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。nginx
在Reactor模式中,有5个关键的参与者:程序员
能够看出,是Reactor管理器并非应用程序负责等待事件、分离事件和调度事件。Reactor并无被具体的事件处理器调度,而是管理器调度具体的事件处理器,由事件处理器对发生的事件做出处理,这就是Hollywood原则。应用程序要作的仅仅是实现一个具体的事件处理器,而后把它注册到Reactor管理器中。接下来的工做由管理器来完成:若是有相应的事件发生,Reactor会主动调用具体的事件处理器,由事件处理器对发生的事件做出处理。golang
应用场景web
场景: 长途客车在路途上,有人上车有人下车,可是乘客老是但愿可以在客车上获得休息。redis
传统作法: 每隔一段时间(或每个站),司机或售票员对每个乘客询问是否下车。编程
Reactor作法:汽车是乘客访问的主体(Reactor),乘客上车后,到售票员(acceptor)处登记,以后乘客即可以休息睡觉去了,当到达乘客所要到达的目的地时(指定的事件发生,乘客到了下车地点),售票员将其唤醒便可。windows
为何使用Reactor后端
网络编程为何要用反应堆?有了I/O复用,有了epoll已经可使服务器并发几十万链接的同时,维持高TPS了,难道这还不够吗?
答案是,技术层面足够了,但在软件工程层面倒是不够的。
程序使用IO复用的难点在哪里呢?
1个请求可能由屡次IO处理完成,但相比传统的单线程完整处理请求生命期的方法,IO复用在人的大脑思惟中并不天然,由于,程序员编程中,处理请求A的时候,假定A请求必须通过多个IO操做A1-An(两次IO间可能间隔很长时间),每通过一次IO操做,再调用IO复用时,IO复用的调用返回里,很是可能再也不有A,而是返回了请求B。即请求A会常常被请求B打断,处理请求B时,又被C打断。这种思惟下,编程容易出错。
形象例子:
传统编程方法就好像是到了银行营业厅里,每一个窗口前排了长队,业务员们在窗口后一个个的解决客户们的请求。一个业务员能够尽情思考着客户A依次提出的问题,例如:
“我要买2万XX理财产品。“
“看清楚了,5万起售。”
“等等,查下我活期余额。”
“余额5万。”
“那就买 5万吧。”
业务员开始录入信息。
”对了,XX理财产品年利率8%?”
“是预期8%,最低无利息保本。“
”早不说,拜拜,我去买余额宝。“
业务员无表情的删着已经录入的信息进行事务回滚。
”下一个!“
用了IO复用则是大师业务员开始挑战极限,在超大营业厅里给客户们人手一个牌子,黑压压的客户们都在大厅中,有问题时举牌申请提问,大师目光敏锐点名指定某人提问,该客户迅速获得大师的答复后,要通过一段时间思考,查查本身的银袋子,咨询下LD,才能再次进行下一个提问,直到获得完整的满意答复退出大厅。例如:大师刚指导A填写转账单的某一项,B又来申请兑换泰铢,给了B兑换单后,C又来办理定转活,而后D与F在争抢有限的圆珠笔时出现了不和谐现象,被大师叫停业务,暂时等待。
这就是基于事件驱动的IO复用编程比起传统1线程1请求的方式来,有难度的设计点了,客户们都是上帝,既不能出错,还不能厚此薄彼。
当没有反应堆时,咱们可能的设计方法是这样的:大师把每一个客户的提问都记录下来,当客户A提问时,首先查阅A以前问过什么作过什么,这叫联系上下文,而后再根据上下文和当前提问查阅有关的银行规章制度,有针对性的回答A,并把回答也记录下来。当圆满回答了A的全部问题后,删除A的全部记录。
在程序中:
某一瞬间,服务器共有10万个并发链接,此时,一次IO复用接口的调用返回了100个活跃的链接等待处理。先根据这100个链接找出其对应的对象,这并不难,epoll的返回链接数据结构里就有这样的指针能够用。接着,循环的处理每个链接,找出这个对象此刻的上下文状态,再使用read、write这样的网络IO获取这次的操做内容,结合上下文状态查询此时应当选择哪一个业务方法处理,调用相应方法完成操做后,若请求结束,则删除对象及其上下文。
这样,咱们就陷入了面向过程编程方法之中了,在面向应用、快速响应为王的移动互联网时代,这样作迟早得把本身玩死。咱们的主程序须要关注各类不一样类型的请求,在不一样状态下,对于不一样的请求命令选择不一样的业务处理方法。这会致使随着请求类型的增长,请求状态的增长,请求命令的增长,主程序复杂度快速膨胀,致使维护愈来愈困难,苦逼的程序员不再敢轻易接新需求、重构。
反应堆是解决上述软件工程问题的一种途径,它也许并不优雅,开发效率上也不是最高的,但其执行效率与面向过程的使用IO复用却几乎是等价的,因此,不管是nginx、memcached、redis等等这些高性能组件的代名词,都义无反顾的一头扎进了反应堆的怀抱中。
反应堆模式能够在软件工程层面,将事件驱动框架分离出具体业务,将不一样类型请求之间用OO的思想分离。一般,反应堆不只使用IO复用处理网络事件驱动,还会实现定时器来处理时间事件的驱动(请求的超时处理或者定时任务的处理),就像下面的示意图:
这幅图有5点意思:
(1)处理应用时基于OO思想,不一样的类型的请求处理间是分离的。例如,A类型请求是用户注册请求,B类型请求是查询用户头像,那么当咱们把用户头像新增多种分辨率图片时,更改B类型请求的代码处理逻辑时,彻底不涉及A类型请求代码的修改。
(2)应用处理请求的逻辑,与事件分发框架彻底分离。什么意思呢?即写应用处理时,不用去管什么时候调用IO复用,不用去管什么调用epoll_wait,去处理它返回的多个socket链接。应用代码中,只关心如何读取、发送socket上的数据,如何处理业务逻辑。事件分发框架有一个抽象的事件接口,全部的应用必须实现抽象的事件接口,经过这种抽象才把应用与框架进行分离。
(3)反应堆上提供注册、移除事件方法,供应用代码使用,而分发事件方法,一般是循环的调用而已,是否提供给应用代码调用,仍是由框架简单粗暴的直接循环使用,这是框架的自由。
(4)IO多路复用也是一个抽象,它能够是具体的select,也能够是epoll,它们只必须提供采集到某一瞬间全部待监控链接中活跃的链接。
(5)定时器也是由反应堆对象使用,它必须至少提供4个方法,包括添加、删除定时器事件,这该由应用代码调用。最近超时时间是须要的,这会被反应堆对象使用,用于确认select或者epoll_wait执行时的阻塞超时时间,防止IO的等待影响了定时事件的处理。遍历也是由反应堆框架使用,用于处理定时事件。
Reactor的几种模式
参考资料:Scalable IO in Java
在web服务中,不少都涉及基本的操做:read request、decode request、process service、encod reply、send reply等。
一、单线程模式
这是最简单的单Reactor单线程模型。Reactor线程是个多面手,负责多路分离套接字,Accept新链接,并分派请求处处理器链中。该模型适用于处理器链中业务处理组件能快速完成的场景。不过这种单线程模型不能充分利用多核资源,因此实际使用的很少。
二、多线程模式(单Reactor)
该模型在事件处理器(Handler)链部分采用了多线程(线程池),也是后端程序经常使用的模型。
三、多线程模式(多个Reactor)
比起第二种模型,它是将Reactor分红两部分,mainReactor负责监听并accept新链接,而后将创建的socket经过多路复用器(Acceptor)分派给subReactor。subReactor负责多路分离已链接的socket,读写网络数据;业务处理功能,其交给worker线程池完成。一般,subReactor个数上可与CPU个数等同。
Proactor是和异步I/O相关的。
在Reactor模式中,事件分离者等待某个事件或者可应用或个操做的状态发生(好比文件描述符可读写,或者是socket可读写),事件分离器就把这个事件传给事先注册的处理器(事件处理函数或者回调函数),由后者来作实际的读写操做。
在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操做(至关于请求),而实际的工做是由操做系统来完成的。发起时,须要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,而后转发完成事件给相应的事件处理者或者回调。
能够看出二者的区别:Reactor是在事件发生时就通知事先注册的事件(读写由处理函数完成);Proactor是在事件发生时进行异步I/O(读写由OS完成),待IO完成事件分离器才调度处理器来处理。
举个例子,将有助于理解Reactor与Proactor两者的差别,以读操做为例(类操做相似)。
在Reactor(同步)中实现读:
- 注册读就绪事件和相应的事件处理器
- 事件分离器等待事件
- 事件到来,激活分离器,分离器调用事件对应的处理器。
- 事件处理器完成实际的读操做,处理读到的数据,注册新的事件,而后返还控制权。
Proactor(异步)中的读:
- 处理器发起异步读操做(注意:操做系统必须支持异步IO)。在这种状况下,处理器无视IO就绪事件,它关注的是完成事件。
- 事件分离器等待操做完成事件
- 在分离器等待过程当中,操做系统利用并行的内核线程执行实际的读操做,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操做完成。
- 事件分离器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据,而后启动一个新的异步操做,并将控制权返回事件分离器。
现行作法
开源C++框架:ACE
开源C++开发框架 ACE 提供了大量平台独立的底层并发支持类(线程、互斥量等)。 同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。 尽管它们都是平台独立的单元,但他们都提供了不一样的接口。ACE Proactor在MS-Windows上不管是性能还在健壮性都更胜一筹,这主要是因为Windows提供了一系列高效的底层异步API。(这段可能过期了点吧) 不幸的是,并非全部操做系统都为底层异步提供健壮的支持。举例来讲, 许多Unix系统就有麻烦。所以, ACE Reactor多是Unix系统上更合适的解决方案。 正由于系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码: 为Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor。真正的异步模式须要操做系统级别的支持。因为事件处理者及操做系统交互的差别,为Reactor和Proactor设计一种通用统一的外部接口是很是困难的。这也是设计通行开发框架的难点所在。
ACE是一个大型的中间件产品,代码20万行左右,过于宏大,一堆的设计模式,架构了一层又一层,使用的时候,要根据状况,看从那一层来进行使用。支持跨平台。
设计模式 :ACE主要应用了Reactor,Proactor等;
层次架构 :ACE底层是C风格的OS适配层,上一层基于C++的wrap类,再上一层是一些框架 (Accpetor,Connector,Reactor,Proactor等),最上一层是框架上服务;
可移植性 :ACE支持多种平台,可移植性不存在问题,听说socket编程在linux下有很多bugs;
事件分派处理 :ACE主要是注册handler类,当事件分派时,调用其handler的虚挂勾函数。实现 ACE_Handler/ACE_Svc_Handler/ACE_Event_handler等类的虚函数;
涉及范围 :ACE包含了日志,IPC,线程池,共享内存,配置服务,递归锁,定时器等;
线程调度 :ACE的Reactor是单线程调度,Proactor支持多线程调度;
发布方式 :ACE是开源免费的,不依赖于第三方库,通常应用使用它时,以动态连接的方式发布动态库;开发难度 :基于ACE开发应用,对程序员要求比较高,要用好它,必须很是了解其框架。在其框架下开发,每每new出一个对象,不知在什么地方释放好。
C网络库:libevent
libevent是一个C语言写的网络库,官方主要支持的是类linux操做系统,最新的版本添加了对windows的IOCP的支持。在跨平台方面主要经过select模型来进行支持。
设计模式 :libevent为Reactor模式;
层次架构:livevent在不一样的操做系统下,作了多路复用模型的抽象,能够选择使用不一样的模型,经过事件函数提供服务;
可移植性 :libevent主要支持linux平台,freebsd平台,其余平台下经过select模型进行支持,效率不是过高;
事件分派处理 :libevent基于注册的事件回调函数来实现事件分发;
涉及范围 :libevent只提供了简单的网络API的封装,线程池,内存池,递归锁等均须要本身实现;
线程调度 :libevent的线程调度须要本身来注册不一样的事件句柄;
发布方式 :libevent为开源免费的,通常编译为静态库进行使用;
开发难度 :基于libevent开发应用,相对容易,具体能够参考memcached这个开源的应用,里面使用了 libevent这个库。
改进方案:模拟异步
在改进方案中,咱们将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的交互细节,从而对外暴露一个完整的异步接口。借此,咱们就能够进一步构建彻底可移植的,平台无关的,有通用对外接口的解决方案。上述方案已经由某公司实现为TProactor。它有两个版本:C++和JAVA的。C++版本采用ACE跨平台底层类开发,为全部平台提供了通用统一的主动式异步接口。
Boost.Asio类库
Boost.Asio类库,其就是以Proactor这种设计模式来实现
须要C/C++ Linux服务器开发学习资料加qun563998835(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享