并发模型之事件驱动模型:其中的之一的Reactor模型

一、并发模型之事件驱动模型(事件驱动模型其中的之一的Reactor模型)

Reactor模式是:事件驱动模型 与IO多路复用的结合。

Reactor模式是一种高效的事件驱动设计模式,广泛应用于高性能服务器、网络编程和异步I/O处理场景,如NginxNettylibevent等。它的核心思想是:事件驱动,依赖于IO多路复用(如select、poll、epoll、kqueue)来监听多个I/O事件,并在事件就绪时通过回调机制分发事件,从而实现高效的并发处理‌。

Reactor模式的主要组件包括:

  • Reactor‌:负责监听I/O事件,并将事件分发给相应的处理器。
  • Acceptor‌:负责接受客户端连接请求(属于IO操作)
  • Handler‌:负责处理具体的业务逻辑

Reactor模式有三种常见的实现方式:

  1. 单线程Reactor结构‌:单个线程既负责事件监听也负责事件处理,适用于低并发场景。
  2. 多线程Reactor结构‌:主线程负责监听事件并分发给多个子线程处理,适用于中高并发场景。
  3. 多Reactor多线程结构‌:主Reactor负责连接监听,子Reactor负责I/O事件处理,适用于超高并发场景‌。

这3种方式:

从方式1=》到方式2:是针对业务逻辑瓶颈问题处理(从单线程=》到多线程处理);

从方式2=》到方式3:不仅针对业务逻辑瓶颈问题处理(从单线程=》到多线程处理) +

                                Reactor容易成为性能瓶颈(IO连接操作)的瓶颈问题处理(从单线程=》到多线程处理);

二、

参考原文链接:一文读懂IO模型和Reactor线程模型_io react 模型-CSDN博客

1、用户空间和内核空间

通俗地讲,内核空间(kernel space)是操作系统内核才能访问的区域,是受保护的内存区域,普通应用程序不能访问。而用户空间(user space)则是普通应用程序访问的内存空间。用户空间和内核空间概念的由来和CPU的发展有很大关系。在CPU的保护模式下,系统需要保护CPU赖以运行的资料;为了保证操作系统内核资料,需要把内存空间进行划分为OS内核运行的空间和普通应用程序运行的空间,两者不能越界。所谓的空间就是内存地址。操作系统为了保护自己不被普通应用程序破坏,对内核空间进行了一些约束,比如访问权限、页的换入换出,优先级等。

目前的操作系统都是采用虚拟存储器。因此内核空间和用户空间都是指的虚拟空间,也就是虚拟地址。

比如对于32位的linux系统而言,用户空间和内核空间划分如下:

32位操作系统的寻址空间(虚拟地址空间)为4G(2的32次方)。在linux中,4G虚拟地址空间中的最高的1G字节空间分配给内核独享使用。低地址的3G空间为应用程序共享,即每个应用程序都有最大3G的虚拟地址空间。每个进程可以通过系统调用切换进入内核,所有进程可以共享Linux内核。因此可以认为每个进程都有4G字节的虚拟空间。

linux内部结构图如下:

2、linux 的五种I/O模型

众所周知,出于对OS安全性的考虑,用户进程是不能直接操作I/O设备的。必须通过系统调用请求操作系统内核来协助完成I/O动作。

下图展示了linux I/O的过程。


操作系统内核收到用户进程发起的请求后,从I/O设备读取数据到kernel buffer中,再将buffer中的数据拷贝到用户进程的地址空间,用户进程获取到数据后返回给客户端。

在I/O过程中,对于输入操作通常有两个不同的阶段:

1)等待数据准备好

2)将数据从内核缓冲区拷贝到用户进程

根据这两个阶段等待方式的不同,可以将Linux I/O分为5种模式:

blocking I/O,阻塞式I/O
nonblocking I/O,非阻塞式I/O
I/O multiplexing (select and poll),I/O多路复用
signal driven I/O (SIGIO),信号驱动I/O
asynchronous I/O (the POSIX aio_functions),异步I/O
对于Socket上的输入操作,第1步通常是等待网络上的数据到达。当数据包到达时,它被复制到内核的缓冲区中。第2步是从内核缓冲区复制数据到应用程序缓冲区。

下面详细介绍Linux中的5种I/O模式

1)blocking I/O
​ 默认情况下,所有的socket都是阻塞式的。下图展示了一个基于UDP的网络数据获取流程。


​ 用户进程调用了recvfrom系统调用,此后一直处于等待状态,直到数据包到达并被拷贝到应用程序缓冲区,或者发生error才返回。整个过程从开始recvfrom调用到它返回一直处于阻塞状态。当recvfrom调用返回后,应用进程才能处理数据。

2)non-blocking I/O
​ 可以设置socket为非阻塞模式。这种设置相当于告诉内核“当I/O操作时,如果请求是不可能完成的,不要把进程进入睡眠状态,返回一个错误即可“。下图展示了整个流程:在前三次调用recvfrom系统调用时,没有就绪的数据返回,所以内核立即返回EWOULDBLOCK错误。第四次调用recvfrom时,数据报已经准备好,它被复制到应用程序缓冲区中,然后recvfrom成功返回。最后应用进程对数据进行处理。当应用程序在一个非阻塞描述符上循环调用recvfrom系统调用时,这种方式也被称为轮询。应用程序不断轮询内核,以查看是否有某些操作准备好了。很明显,这通常会浪费CPU时间,但这种模式偶尔也会被使用。通常在专门用于一个功能的系统上使用。


3)I/O multiplexing
​ I/O多路复用通常使用select或者poll系统调用。这种方式下的阻塞只是被select或者poll这两个系统调用阻塞,而不会阻塞实际的I/O系统调用。下图展示了整个过程。当调用select时,应用进程被阻塞。同时,系统内核会“监视”所有select负责的socket。只要其中有1个socket的数据准备好了,select调用就返回。然后调用recvfrom将数据报复制到应用程序缓冲区,最后返回给用户进程。

​ 这种方式和blocking I/O相比似乎更差,因为整个过程产生了2次系统调用,select和recvfrom。但是使用select的好处是可以同时等待多个描述符准备好。换句话说可以同时“聆听”多个socket通道,同时处理多个连接。select的优势不是对于单个连接处理得更快,而是能同时处理更多的连接。这和多线程阻塞式I/O有点类似。只不过后者是使用多个线程(每个文件描述符对应一个线程)来处理I/O,每个线程都可以自由地调用阻塞式系统调用,比如recvfrom。我们知道线程多了会带来上下文切换的开销,因此未必优于select方式。在前面Java NIO 的例子中,我们已经体会到了selector带来的性能提升。

Linux内核将所有外部设备都当成一个个文件来操作。我们对文件的读写都通过调用内核提供的系统调用;内核给我们返回一个文件描述符(file descriptor)。而对一个socket的读写也会有相应的描述符,称为socketfd。应用进程对文件的读写通过对fd的读写完成。

4)signal driven I/O
​ 信号驱动方式就是等数据准备好后,由内核发出SIGIO信号通知应用进程。示意图如下:


​ 应用进程通过sigaction系统调用建立起SIGIO信号处理通道,然后此系统调用就返回,不阻塞。当数据准备好后,内核会产生一个SIGIO信号通知到应用进程。此时既可以使用SIGIO信号处理器通过recvfrom系统调用读取数据,然后通知应用进程数据准备好了,可以处理了;也可以直接通知应用进程读取数据。不管使用何种方式,好处都是应用进程不会阻塞,可以继续执行,只要等待信号通知数据准备好被处理了、数据准备好被读取了。

5)asynchronous I/O
​ 异步I/O是由POSIX规范定义的。和信号驱动I/O模型的区别是前者内核告诉我们何时可以开始一个I/O操作,而后者内核会告诉我们一个I/O操作何时完成。示意图如下:


当用户进程发起系统调用后会立刻返回,并把所有的任务都交给内核去完成,不会被阻塞等待I/O完成。内核完成之后,只需返回一个信号告诉用户进程已经完成就可以了。

五种I/O模式可以从同步、异步,阻塞、非阻塞两个维度来划分:


3、线程模型

线程模型通常是指线程的使用方式。在Java I/O中,主要有2种线程模型。

1)传统的阻塞式I/O
​ 正如我们前面写的传统IO通信案例版本4。在版本4的例程中,为了同时处理多个客户端的请求,服务端为每一个连接都会分配一个新的线程处理。这个独立的线程完成数据的读写和业务处理。这虽然是"传统"的处理方式,但是也是最经典的IO线程模型。示意图如下:

该模型采用阻塞式IO,连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read 操作,造成线程资源浪费。

当并发数很大,就会创建大量的线程,占用大量系统资源。

2)Reactor模式
Reactor模式针对传统IO的缺点,提出了解决方案。

方案1:基于 I/O 复用模型。即多个连接共用一个阻塞对象,当某个连接有新的数据准备好时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

方案2:基于线程池复用线程资源,不需要给每个连接创建一个线程。将连接完成后的业务处理任务分配给线程池中的线程进行处理。这样一个线程可以处理到多个客户端的业务。

总结一句话,I/O 多路复用+线程池,就是所谓的"Reactor 模式"的基本设计思想。其实我们前面NIO案例中的版本1的实现方式就有点这种味道,只不过不是严格意义上的Reactor模式罢了。

Reactor模式中的两个核心组件

组件1:Reactor。Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 I/O 事件做出反应。

组件2:Handlers。完成实际 I/O 事件中数据的读写和要做的一系列业务处理。

根据Reactor的数量和业务处理线程池线程数量不同,又分为3种具体实现。

2.1)单Reactor单线程
Reactor对象通过I/O复用模型(在Java NIO中就是使用Selector)监控客户端请求事件,收到事件后通过dispatch进行分发。如果是建立连接请求事件,则由acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后的数据读->业务处理->写。

注意,上述过程都是发生在一个线程里,只不过是非阻塞方式。工作原理示意图如下:

这种方式,服务器端使用一个线程基于多路复用就完成了所有的 IO 操作(包括连接,读数据、业务处理、写数据等),没有多线程间通信、竞争的问题,实现简单。但是如果客户端连接数较多,将无法支撑。因为只有一个线程,不能完全发挥多核 CPU 的性能。且Handler在处理某个连接上的业务时,整个线程无法处理其他连接事件。如果业务处理很耗时,很容易会导致性能瓶颈。如果线程意外终止,或者进入死循环,会导致整个系统不可用。

2.2)单Reactor多线程
为了克服上述模型的缺点,我们可以考虑将非IO操作从Reactor 线程的处理中移出,来提升Reactor 线程的性能。

具体说明如下:

2.2.1)Reactor对象通过select 监控client端的请求事件, 收到事件后,通过dispatch进行分发。

2.2.2)如果是连接建立请求,则由acceptor通过accept处理连接请求,然后分配一个Handler对象处理完成连接后的数据读写。

2.2.3)如果不是连接请求,则由reactor分发(dispatch)给连接对应的Handler来处理。

2.2.4)Handler只负责响应IO事件,不做具体的业务处理。read数据后,会分发给Worker线程池的某个线程进行业务逻辑处理。

2.2.5)Worker线程池会分配单独的线程完成真正的业务处理,包括编解码、逻辑计算,完成处理后将结果数据返回给handler。

2.2.6)Handler收到响应后,通过send将数据返回给client端。

这种模型下,Reactor线程只负责处理所有的事件的监听和响应(数据读、写),而不参与数据的业务处理(数据编解码、逻辑处理)。业务处理的任务交给线程池中的线程处理,提高了并发性能,特别是在业务复杂的情况下。工作原理示意图如下:


2.3)主从Reactor多线程
上述单Reactor多线程模型虽然可以充分压榨CPU的性能,但是由于Reactor是单线程运行的,所以在高并发场景下Reactor容易成为性能瓶颈。可以考虑让Reactor在多线程中运行,这就是多Reactor模型,也叫主从Reactor模型。

具体说明如下:

2.3.1)Reactor主线程mainReactor通过select 监听连接事件,收到事件后,通过acceptor处理连接事件。

2.3.2)当acceptor处理连接事件后,mainReactor将连接分配给subReactor 。subReactor是Reactor的子线程,和mainReactor不在一个线程中。

2.3.3)subReactor将连接加入到连接队列进行监听,并负责创建handler进行各种事件的处理(数据的读、写)。

2.3.4)subReactor也通过select监听,当有新事件发生时,subreactor就会调用对应的handler处理。

2.3.5)handler只负责数据的I/O,针对数据的业务处理还是由worker线程池中的线程处理,并返回结果。

2.3.6)handler收到worker线程的响应数据后,通过send将结果数据返回给client。

Reactor主线程可以对应多个Reactor子线程,即MainRecator能关联多个SubReactor。和worker线程池一样,线程数都能配置。

这种方式的优点非常明显,就是减轻了mainRecator的负担,让其只负责处理连接请求,不包含I/O的处理。后续的处理统统交给SubReactor。主、从Reactor分别运行在不同的线程中,且线程数可以配置。业务处理还是交给worker线程池中的线程执行。

主从Reactor线程模型在许多项目中都有应用,比如Nginx的主从Reactor多进程模型、Netty的主从多线程模型等。

其工作原理示意图如下(注意观察和上面一个图的区别):



                        
原文链接:https://blog.csdn.net/hellozpc/article/details/101109105

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值