【Reactor模型】事件驱动模型 - Reactor模型简述

模型演进

抛出一个问题:线程怎样才能高效地处理多个连接的业务?

当一个连接对应一个线程时,线程一般采用[read->业务处理->send]的处理流程,如果当前连接没有数据可读,那么线程会阻塞到read操作上(socket默认情况是阻塞IO),不过这种阻塞方式并不影响其他线程。

但是引入线程池,一个线程需要处理多个连接的业务,线程在处理某个连接的read的操作时,如果遇到没有数据可读,就会发生阻塞,那么线程就没办法继续处理其他的业务。

为解决这个问题,最简单的方式就是将socket改成非阻塞,然后线程不断地轮询调用read操作来判断是否有数据,但是这样做轮询的效率非常低。

最合适的方法是只有当连接上有数据的时候,线程才回去发起读请求,这一技术就是I/O多路复用。在Linux系统中select/poll/epoll就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。

select/poll/epoll是如何获取网络事件的呢?

  • 如果没有事件发生,线程只需阻塞在这个系统调用,而无需像前面的线程池方案那样轮询调用read操作来判断是否有数据
  • 如果有事件的发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中在处理这些对应的业务即可

而Reactor模式就是基于面向对象的思想,对I/O多路复用做了一层封装,让使用者不用考虑底层网络API的细节,只需关注应用代码的编写。Reactor指的是对事件的反应,也就是来了一个事件,Reactor就又相对应的反应/响应。

Reactor模式也叫Dispatcher模式,即I/O多路复用监听事件,收到时间后,根据事件类型分配(Dispatch)给某个进程/线程

Reactor模式

Reactor模式主要由Reactor和处理资源池这两个核心部分组成,负责:

  • Reactor负责监听和分发事件,事件类型包含连接事件,读写事件;
  • 处理资源池负责处理事件,如read->处理逻辑->send

Reactor模式是灵活多变的,可以应对不同的业务场景,灵活在于:

  • Reactor的数量可以只有一个,也可以有多个;
  • 处理资源池可以是单个进程/线程,也可以是多个进程/线程

理论上有四种方案可选:

  • 单Reactor单进程/线程
  • 单Reactor多进程/线程
  • 多Reactor单进程/线程
  • 多Reactor多进程/线程

由于Reactor单进程/线程没有性能优势,因此实际中并没有应用。剩下三个方案都比较经典,用处较多。

单Reactor单进程/线程

方案示意图:

image

可以看到进程中有Reactor、Accecptor、Handler这三个对象:

  • Reactor对象的作用是监听和分发事件
  • Acceptor对象的作用是获取连接
  • Handler对象的作用是处理业务

对象里的select,accept,read,send是系统调用函数,dispatch和业务处理是需要完成的操作,其中dispatch是分发事件操作。

单Reactor单进程方案描述:

  • Reactor对象通过select(IO多路复用接口)监听事件,收到事件后通过dispatch进行分发,具体分发给Acceptor对象还是Handler对象,要看事件类型;
  • 如果是连接建立事件,则交由Acceptor对象进行处理,Acceptor对象会通知accept方法获取连接,并创建一个Handler对象来处理后续响应事件。
  • 如果不是连接建立事件则交由连接对应的Handler对象来进行响应;
  • Handler对象通过read->业务处理->send来完成完整的业务流程

方案缺点:

  • 因为只有一个进程,无法充分利用多核CPU性能;
  • Handler对象在业务处理时,整个进程是无法处理其他连接收件的,如果业务处理耗时比较长,就会造成响应的延迟。

所以单Reactor单进程的方案不适用于计算机密集型的场景,只是用于业务处理非常快速的场景。Redis就是采用的此种方案。

单Reactor多进程/线程

单Reactor多进程方案示意图:

img

单Reactor多线程方案描述

  • Reactor对象通过select(IO多路复用接口)监听事件,收到事件后通过dispatch进行分发,具体分发给Accept对象还是Handler对象,还要看收到的事件类型。

  • 如果是连接建立的事件,则交由Acceptor对象进行处理,Acceptor对象会通过accept方法获取连接,并创建一个handler对象来处理后续的响应事件

  • 如果不是连接建立事件,则交由当前连接对应的handler对象进行响应

  • Handler对象不再负责业务处理,只负责数据的接收和发送,Handler对象通过read读取到数据后,会将数据发给子线程里的Processor对象进行业务处理;

  • 子线程里的processor对象就进行业务处理,处理完成后,将结果发给主线程中的Handler对象,接着由Handler通过send方法将响应结果发送给client;

方案优势

能够充分利用多核CPU的能力

方案缺点

由于使用多线程,所以会带来多线程竞争资源的问题。比如:子线程在完成业余处理后,要把结果传递给主线程的Reactor进行发送,这里就涉及到共享数据的竞争。

单Reactor多进程方案:

相比多线程,多进程方案需要考虑子进程<->父进程的双向通信,并且父进程还需要知道子进程要将数据发送给哪一个客户端。因为多线程间可以共享数据,虽然要额外考虑并发问题,但是这远比进程间通信复杂度低的多。

单Reactor还存在一个问题,因为一个Reactor对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发场景时,容易成为性能的瓶颈的地方。

多Reactor多进程/线程

多Reactor多线程方案示意图:

img

多Reactor多线程方案描述

  • 主线程中MainReactor对象通过select监控连接建立事件,收到时间后通过Acceptor对象中的accept获取连接,将新的连接分配给某个子线程;
  • 子线程中的SubReactor对象将MainReactor对象分配到连接加入select继续进行监听,并创建一个Handler用于处理连接的响应事件
  • 如果有新的事件发生,SubReactor对象会调用当前连接对应的Handler对象来进行响应
  • Handler对象通过read->业务处理->send的流程来完成完整的业务流程。

多Reactor多线程方案相比单Reactor多线程方案实现要简单,原因如下:

  • 主线程和子线程分工明确,主线程只负责接收新链接,子线程负责完成后续的业务处理。
  • 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无需返回数据,直接就可以在子线程将处理结果发送给客户端

采用多Reactor多线程/进程方案的开源软件有Nginx















参考文章:

https://www.zhihu.com/question/26943938

posted @ 2023-01-26 17:54  Emma1111  阅读(199)  评论(0编辑  收藏  举报