高性能网络模式:Reactor 和 Proactor

news2025/2/28 6:20:49

文章目录

    • 演进
    • 多 Reactor 多进程 / 线程
    • Proactor
    • 总结

演进

如果要让服务器服务多个客户端,那么最直接的方式就是为每一条连接创建线程。其实创建进程也是可以的,原理是一样的,进程和线程的区别在于线程比较轻量级些,线程的创建和线程间切换的成本要小些,为了描述简述,后面都以线程为例。

处理完业务逻辑后,随着连接关闭后线程也同样要销毁了,但是这样不停地创建和销毁线程,不仅会带来性能开销,也会造成浪费资源,而且如果要连接几万条连接,创建几万个线程去应对也是不现实的。

要这么解决这个问题呢?我们可以使用 「资源复用」 的方式。

也就是不用再为每个连接创建线程,而是创建一个 「线程池」,将连接分配给线程,然后一个线程可以处理多个连接的业务。

不过,这样又引来一个新的问题,线程怎样才能高效地处理多个连接的业务?

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

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

要解决这一个问题,最简单的方式就是将 socket 改成非阻塞,然后线程不断地轮询调用 read 操作来判断是否有数据,这种方式虽然该能够解决阻塞的问题,但是解决的方式比较粗暴,因为轮询是要消耗 CPU 的,而且随着一个线程处理的连接越多,轮询的效率就会越低。

上面的问题在于,线程并不知道当前连接目前是否有数据可读,从而需要每次通过 read 去试探。

那有没有办法在只有当连接上有数据的时候,线程才去发起读请求呢?答案是有的,实现这一技术的就是 I/O 多路复用。

I/O 多路复用技术会用一个系统调用函数来监听我们所有关心的连接,也就说可以在一个监控线程里面监控很多的连接。

在这里插入图片描述

当下开源软件能做到网络高性能的原因就是 I/O 多路复用吗?

是的,基本是基于 I/O 多路复用,用过 I/O 多路复用接口写网络程序的同学,肯定知道是面向过程的方式写代码的,这样的开发的效率不高。

于是,大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。

大佬们还为这种模式取了个让人第一时间难以理解的名字: Reactor 模式

Reactor 翻译过来的意思是 「反应堆」,可能大家会联想到物理学里的核反应堆,实际上并不是的这个意思。

这里的反应指的是 「对事件反应」,也就是来了一个事件,Reactor 就有相对应的反应/响应。

事实上,Reactor 模式也叫 Dispatcher 模式,我觉得这个名字更贴合该模式的含义,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程

Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下:

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

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

  • Reactor 的数量可以只有一个,也可以有多个;

  • 处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;
    将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择:

  • 单 Reactor 单进程 / 线程;

  • 单 Reactor 多进程 / 线程;

  • 多 Reactor 单进程 / 线程;

  • 多 Reactor 多进程 / 线程;
    其中,「多 Reactor 单进程 / 线程」实现方案相比「单 Reactor 单进程 / 线程」方案,不仅复杂而且也没有性能优势,因此实际中并没有应用。

剩下的 3 个方案都是比较经典的,且都有应用在实际的项目中:

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

方案具体使用进程还是线程,要看使用的编程语言以及平台有关:

  • Java 语言一般使用线程,比如 Netty
  • C 语言使用进程和线程都可以,例如 Nginx 使用的是进程,Memcache 使用的是线程

多 Reactor 多进程 / 线程

在这里插入图片描述
方案详细说明如下:

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

多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:

  • 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。
  • 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。
    大名鼎鼎的两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。

采用了「多 Reactor 多进程」方案的开源软件是 Nginx,不过方案与标准的多 Reactor 多进程有些差异。

具体差异表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程


Proactor

前面提到的 Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式

这里先给大家复习下阻塞、非阻塞、同步、异步 I/O 的概念。

先来看看 阻塞 I/O,当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。

注意,阻塞等待的是 「内核数据准备好」「数据从内核态拷贝到用户态」 这两个过程,过程如下图:

在这里插入图片描述

知道了阻塞 I/O ,来看看 非阻塞 I/O,非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。过程如下图:

在这里插入图片描述
注意:这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程

举个例子,如果 socket 设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问,而不做任何设置的话,默认是阻塞 I/O。

因此,无论 read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用。因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。

而真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。

当我们发起 aio_read (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图:

在这里插入图片描述

举个你去饭堂吃饭的例子,你好比应用程序,饭堂好比操作系统。

阻塞 I/O 好比,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等啊等,等了好长一段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。

非阻塞 I/O 好比,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过几十分钟,你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的。

异步 I/O 好比,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待。

很明显,异步 I/O 比同步 I/O 性能更好,因为异步 I/O 在「内核数据准备好」和「数据从内核空间拷贝到用户空间」这两个过程都不用等待。

Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型。

现在我们再来理解 Reactor 和 Proactor 的区别,就比较清晰了。

  • Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
  • Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。

因此,Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。这里的「事件」就是有新连接、有数据可读、有数据可写的这些 I/O 事件这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。

举个实际生活中的例子,Reactor 模式就是快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快递。而在 Proactor 模式下,快递员直接将快递送到你家门口,然后通知你。

无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件

接下来,一起看看 Proactor 模式的示意图:

在这里插入图片描述
可惜的是,在 Linux 下的异步 I/O 是不完善的, aio 系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的 socket 是不支持的,这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。

而 Windows 里实现了一套完整的支持 socket 的异步编程接口,这套接口就是 IOCP,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案


总结

常见的 Reactor 实现方案有三种:

  • 第一种方案单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis(6.0之前 ) 采用的是单 Reactor 单进程的方案。

  • 第二种方案单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。

  • 第三种方案多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。

Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」

因此,真正的大杀器还是 Proactor,它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。

不过,无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/155547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Dash搭建可视化网站】项目12:全球恐怖主义数据大屏制作步骤详解

全球恐怖主义数据大屏制作步骤详解1 项目效果图2 项目架构3 文件介绍和功能完善3.1 assets文件夹介绍3.2 app.py和index.py文件完善3.3 header.py文件完善3.4 filteritem.py文件完善3.5 api.py文件和api.ipynb文件完善3.6 staclbarline.py文件完善3.7 piechart.py文件完善3.8 m…

IO多路复用之select、poll、epoll之间的区别总结

一、IO多路复用基本概念 select、poll、epoll都是IO多路复用的机制。IO多路复用就是通过一种机制,让一个进程/线程可以监视多个描述符,一旦某个描述符就绪(一般是读写就绪),能够通知应用程序进行相应的读写操作。 I/…

并网逆变器学习笔记5---三电平DPWM

参考文献:《中压三电平全功率风电变流器关键技术研究---任康乐》 1、调制策略分析 DPWM由于其在任意时刻均有一相钳位在某个电平,使得该相的功率器件不发生开关动作,因而可以大大降低开关损耗(平均降低1/3)&#xff…

Java多线程案例——定时器

一,定时器1.定时器的概念定时器是Java开发中一个重要的组件(功能类似于闹钟),可以指定一个任务在多长时间后执行(尤其在网络编程的时候,如果网络卡顿很长时间没有响应用户的需求,此时可以使用定…

分享|UWB使用频段大幅收窄,新标准对于行业发展是好是坏?

近日,工信部无线电管理局发布了《超宽带(UWB)设备无线电管理规定(征求意见稿)》(以下简称“新版《规定》”)。 根据新版《规定》,未来国内UWB技术的使用频段为:7235-875…

seo的基本知识(概述网站内部优化和外部优化)

了解网站外部优化的4大重点 网站优化的时候都会重视网站的外部优化,所以网站外部优化的4大重点!今天就来和大家说一说! 1.高质量的内容和外链 未来的SEO道路高质量的有价值的内容是非常重要的,还有就是高质量的外链也是重要之…

北大硕士LeetCode算法专题课-查找相关问题

黑马算法面试专题 北大硕士LeetCode算法专题课-字符串相关问题 北大硕士LeetCode算法专题课-数组相关问题_​​​​​​ 北大硕士LeetCode算法专题课-基础算法查找_ 北大硕士LeetCode算法专题课-基础算法之排序_客 北大硕士LeetCode算法专题课---算法复杂度介绍_…

Neo4j框架学习之一安装和使用

文章目录1、何为Neo4j2、安装和使用2.1 安装2.2 基础概念1、何为Neo4j ​ Neo4j是一个高性能的NOSQL图形数据库,是一个嵌入式的、基于磁盘的,数据结果为网格(图)、具备完全的事务特性的Java持久化引擎。 数据结构 ​ 在一个图中包含两种基本的数据类型…

从浏览器里输入URL构建你的前端知识体系

嗨!我是团子,好久不见~ 记得22年寒假复习八股的时候,一直在苦恼怎样才能把八股的内容真正的转换为自己的知识。毕竟光靠死记硬背每个知识点,是不能在面试中给面试官留下不错的印象的。后面在整理《浏览器里输入URL后发生了什么》…

Stellarium 1.2 正式发布

导读Stellarium 1.2 已发布。Stellarium 是一款免费开源 GPL(自由软件基金会 GNU 通用公共许可证)软件,它使用 OpenGL 图形接口对星空进行实时渲染。 软件可以模拟肉眼、双筒望远镜和小型天文等观察天空,根据观测者所处时间和位置…

项目管理:项目经理如何创建项目日程计划表

当项目经理接手项目后,要做好项目的日程安排,这是决定项目是否成功完成的最重要任务之一。 项目经理都希望项目按照制定好的进度计划完工,但在实际的情况中,总会有那么一两个项目会出现进度延迟的情况,管理者可以使用…

忆享科技戟星安全实验室|OSS的STS模式授权案例

戟星安全实验室忆享科技旗下高端的网络安全攻防服务团队.安服内容包括渗透测试、代码审计、应急响应、漏洞研究、威胁情报、安全运维、攻防演练等。本文约957字,阅读约需3分钟。前言《漏洞挖掘系列》将作为一个期刊持续更新,我们会将项目中所遇到的觉得有…

图像编辑Photoshop 2023中文新

Photoshop2023从照片编辑和合成到数字绘画、动画和图形设计-只要能想到,就能在Photoshop中创作出来。相信大家都有在用之前的版本,这款软件功能丰富,实用性很强,有着大量的功能用户都可以用上,不管是美化还是滤镜&…

基于冲突搜索(CBS)的多智能体路径寻优(MAPF)

1 背景 1.1 问题描述 多智能体路径寻优( Multi-Agent Path Finding,MAPF )问题由一个无向无权图G ( V ,E )和一组k个智能体组成,其中智能体有起始点和目标点。时间被离散化为时间步。在连续的时间步之间,每个智能体既可以移动到…

Kafka生产者——消息发送流程,同步、异步发送API

生产者消息发送流程 发送原理 Kafka的Producer发送消息采用的是异步发送的方式。 在消息发送的过程中,涉及到了两个线程:main线程和Sender线程,以及一个线程共享变量:RecordAccumulator。 ①main线程中创建了一个双端队列RecordAccumulator&#xff0c…

Spring Boot 创建和使用

Spring Boot 创建和使用一、什么是 Spring Boot二、Spring Boot 优点三、Spring Boot 项目创建3.1 使用 Idea 创建验证3.2 网页版创建四、项目目录介绍五、约定大于配置 (重要)5.1 启动类5.2 自定义类在目录中的位置一、什么是 Spring Boot Spring 的诞⽣是为了简化 Java 程序…

《架构300讲》学习笔记(51-100)

前言 内容来自B站IT老齐架构300讲内容。 053动静分离 静态数据:无个性化的数据,静态文件,低频变动的数据。 动态数据:个性化推荐,高频写。 有效的区分页面中的动静数据是优化的关键前提。 页面伪静态化技术&#x…

【Leetcode】308. 二维区域和检索 - 可变

一、题目 1、题目描述 给你一个 2D 矩阵 matrix,请计算出从左上角 (row1, col1) 到右下角 (row2, col2) 组成的矩形中所有元素的和。 实现 NumMatrix 类: NumMatrix(int[][] matrix) 用整数矩阵 matrix 初始化对象。void update(int row, int col, i…

OpenCv相机标定——圆形标定板标定

提取角点时与黑白棋盘格差别主要在于寻找角点的函数,只需将第一章内第二段代码 ret, corners1 cv.findChessboardCorners(img_gray, (w, h)) # 寻找内角点改为 ret, corners1 cv.findCirclesGrid(img_gray, (w, h)) # 寻找内角点,更详细的内容参考第一…

盘点| 能够实现小程序开发提效的框架/工具有这些

近年来,为了研发效率的提升,技术高频革新,开发者们纷纷表示:“好是好,就是快学不动了!”。开发者们在不断学习新语言、框架、工具等内容的同时,也在担心所学是否真正有用。而小程序其实能够帮助…