【九】高性能IO模型----Reactor与proactor

参考

Reactor与Proactor

高性能网络编程6--reactor反应堆与定时器管理

两种高效的服务器设计模型:Reactor和Proactor模型

一、Reactor简介

      Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O

中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;

一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。
  Reactor是一种事件驱动机制,和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,

而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。

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

二、为什么使用Reactor

网络编程为什么要用反应堆?有了I/O复用,有了epoll已经可以使服务器并发几十万连接的同时,维持高TPS了,难道这还不够吗?

答案是,技术层面足够了,但在软件工程层面却是不够的。

程序使用IO复用的难点在哪里呢?

      1个请求可能由多次IO处理完成,但相比传统的单线程完整处理请求生命期的方法,IO复用在人的大脑思维中并不自然,因为,程序员编程中,处理请求A的时候,假定A请求必须经过多个IO操作A1-An(两次IO间可能间隔很长时间),每经过一次IO操作,再调用IO复用时,IO复用的调用返回里,非常可能不再有A,而是返回了请求B。即请求A会经常被请求B打断,处理请求B时,又被C打断。这种思维下,编程容易出错。

形象例子:
本部分和下部分内容来自:高性能网络编程6--reactor反应堆与定时器管理

    传统编程方法就好像是到了银行营业厅里,每个窗口前排了长队,业务员们在窗口后一个个的解决客户们的请求。一个业务员可以尽情思考着客户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模式中,有5个关键的参与者:

描述符(handle):由操作系统提供的资源,用于识别每一个事件,如Socket描述符、文件描述符、信号的值等。在Linux中,它用一个整数来表示。事件可以来自外部,如来自客户端的连接请求、数据等。事件也可以来自内部,如信号、定时器事件。

同步事件多路分离器(event demultiplexer):事件的到来是随机的、异步的,无法预知程序何时收到一个客户连接请求或收到一个信号。所以程序要循环等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用I/O复用技术实现。在linux系统上一般是select、poll、epol_waitl等系统调用,用来等待一个或多个事件的发生。I/O框架库一般将各种I/O复用系统调用封装成统一的接口,称为事件多路分离器。调用者会被阻塞,直到分离器分离的描述符集上有事件发生。

事件处理器(event handler):I/O框架库提供的事件处理器通常是由一个或多个模板函数组成的接口。这些模板函数描述了和应用程序相关的对某个事件的操作,用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数一般声明为虚函数,以支持用户拓展。

具体的事件处理器(concrete event handler):是事件处理器接口的实现。它实现了应用程序提供的某个服务。每个具体的事件处理器总和一个描述符相关。它使用描述符来识别事件、识别应用程序提供的服务。

Reactor 管理器(reactor):定义了一些接口,用于应用程序控制事件调度,以及应用程序注册、删除事件处理器和相关的描述符。它是事件处理器的调度核心。 Reactor管理器使用同步事件分离器来等待事件的发生。一旦事件发生,Reactor管理器先是分离每个事件,然后调度事件处理器,最后调用相关的模 板函数来处理这个事件。

可以看出,是Reactor管理器并不是应用程序负责等待事件、分离事件和调度事件。Reactor并没有被具体的事件处理器调度,而是管理器调度具体的事件处理器,由事件处理器对发生的事件作出处理。应用程序要做的仅仅是实现一个具体的事件处理器,然后把它注册到Reactor管理器中。接下来的工作由管理器来完成:如果有相应的事件发生,Reactor会主动调用具体的事件处理器,由事件处理器对发生的事件作出处理。

四、Reactor的几种模式

1 单线程模式

 

select会一直监听着事件,事件来了之后给dispatch分发,如果建立请求的事件则分配的acceptor,由acceptor创建一个handler来处理后续的业务,如果不是建立请求的事件则分配个之前对应的handler来处理后续业务
这个情况的优点就是简单。。。没有多线程共享资源争抢导致的问题。缺点就是就单线程,浪费了多CPU,并且同一时刻只有一个handler能处理,其他的得等着。
听起来好像没啥用啊这样,是的绝大部分场景不适合,但是redis就是这样用的。因为它处理业务够快。所以这种适合在业务处理极快的情况下使用。

2.单Reactor多线程

当业务处理不快就上多线程咯。
这个模式和上面的区别就在于具体业务实现不由handler处理的,handler只负责read数据,将数据给业务线程,然后业务线程处理完毕之后返回结果给handler,由handler send给客户端。
这个模式的优点就是可以充分利用CPU,适合业务处理不快的情况。缺点就是多线程之间共享资源的争抢产生的问题,并且只有一个Reactor来监听并响应,当请求量太大时,一个Reactor可能会成为性能瓶颈。

3.多Reactor多线程

 

mainReactor主要用来接受连接,由连接来就给acceptor,acceptor将新的连接分配个某个subReactor,然后这个subReactor将其加入自己的监听列表,并创建一个handler来处理这个连接。之后就都由这个subReactor来select监听来响应这个连接的请求,然后dispatch给对应的handler来read,业务处理,send。mainReactor就不管啦。
所以这种方案就等于主Reactor分流了,只有新的连接由主Reactor接受,老的连接都分给了subReactor来响应。

五、proactor模型

 Proactor是和异步I/O相关的。

       在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离器就把这个事件传给事先注册的处理器(事件处理函数或者回调函数),由后者来做实际的读写操作。
       在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。

       可以看出两者的区别:

        Reactor是在事件发生时就通知事先注册的事件(读写由处理函数完成)

        Proactor是在事件发生时进行异步I/O(读写由OS完成),待IO完成事件分离器才调度处理器来处理。

 举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。

在Reactor(同步)中实现读:
 - 注册读就绪事件和相应的事件处理器
 - 事件分离器等待事件
 - 事件到来,激活分离器,分离器调用事件对应的处理器。
 - 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章