Proactor 与 Reactor

Reactor模式用于同步I/O,Proactor运用于异步I/O操作

阻塞模型

同步和异步

针对应用程序和内核交互而言

  • 同步:用户进程触发I/O操作,等待或轮询去查看I/O操作是否就绪
  • 异步:用户进程触发I/O操作后,去做自己的事情,I/O操作完成后给予通知

阻塞和非阻塞

进程在访问数据的时候,根据I/O操作的就绪状态采取不同的方式,即读取或写入操作函数的实现方式

  • 阻塞:读取或写入函数将一直等待
  • 非阻塞:读取或写入函数回立即返回一个状态值

常见I/O模型

以小明下载王者农药和打开游戏两个任务为例

同步阻塞

点击下载—盯着下载条—到100%下载完成—打开游戏

  • 同步:等待下载完成的结果通知
  • 阻塞:盯着下载条,没有处理其余任务

用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行

同布非阻塞

点击下载—刷抖音/看B站—时不时返回查看下载进度—下载完成:点击打开游戏

  • 同步:等待下载完成的结果通知
  • 非阻塞:下载过程可以去处理其余任务

用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费

异步阻塞

小明开启了下载完成的震动通知

点击下载—单纯等待,不主动查看—收到震动通知—打开游戏

  • 异步:下载结果以震动的方式告知
  • 阻塞:等待这个震动,没有去做别的

发起I/O后足赛等待,根据到达的通知来进行数据处理

异步非阻塞

小明开启了下载完成的震动通知

点击下载—刷抖音/看B站—收到震动通知—打开游戏

发起I/O后立即返回,等I/O完成后回得到完成通知,用户进程根据通知来进行数据处理

归纳

同步/异步关注的是消息通知的机制

阻塞/非阻塞关注的是程序(线程)等待消息通知时的状态

Reactor和Proactor

阻塞与非阻塞都可以理解为同步范畴下才有的概念,对于异步,就不会再去分阻塞非阻塞。

Reactor

要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生

  • 有的话就立即将事件通知工作线程(逻辑单元)数据的读写,接受新的连接以及处理客户请求均在工作线程中完成;
  • 除此之外,逻辑线程不作任何工作。

中心思想:

  • 将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;
  • 一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。

Reactor是一种事件驱动机制,和普通函数调用的不同之处在于:

  • 应用程序不是主动的调用某个API完成处理,而是恰恰相反
  • Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。用“好莱坞原则”来形容Reactor再合适不过了:不要打电话给我们,我们会打电话通知你。

应用场景

场景: 长途客车在路途上,有人上车有人下车,但是乘客总是希望能够在客车上得到休息。

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

Reactor做法:汽车是乘客访问的主体(Reactor),乘客上车后,到售票员(acceptor)处登记,之后乘客便可以休息睡觉去了,当到达乘客所要到达的目的地时(指定的事件发生,乘客到了下车地点),售票员将其唤醒即可。

组成

Reactor模式是基于事件驱动的分发处理模型 有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers 这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。

Proactor

Proactor将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。

  1. 事件句柄初始化一个异步读操作,此时该句柄并不在意异步操作结果,而是要获得完成事件而注册
  2. 事件多路器等待直到io事件完成
  3. 当事件多路器等待io事件时,操作系统在一个并行的内核线程上处理读操作,并将数据放到一个用户定义的缓冲中,并通知事件多路器操作完成。
  4. 事件多路器调用事件句柄
  5. 事件句柄从用户定义缓冲中获得用户数据并操作,然后开始新的异步操作并将控释返回事件多路器

在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离器就把这个事件传给事先注册的处理器(事件处理函数或者回调函数),由后者来做实际的读写操作。

区别

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

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

在Reactor(同步)中实现读:

  • 注册读就绪事件和相应的事件处理器
  • 事件分离器等待事件
  • 事件到来,激活分离器,分离器调用事件对应的处理器。
  • 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

Proactor(异步)中的读:

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

Golang并发模型

golang写服务器不需要在用reactor模式的epoll了,因为golang的协程非常廉价,可以并发开启成千上完个协程。

一个协程占用内存大概2KB左右,一个线程占用内存大概2MB左右,一个线程抵1000个协程。

由自身的GMP模型来实现

参考


分享: