如何用一个例子向10岁小孩解释高并发实时服务的单线程事件循环架构

I/O密集型进程和CPU密集型进程

聊天应用程序、MMO(大型多人在线)游戏、金融交易系统、等实时服务需要处理大量并发流量和实时数据。

这些服务是I/O密集型的,因为它们花费大量资源处理输入输出操作,例如高吞吐量、低延迟网络通信(客户端与服务器以及其他应用程序组件之间)、实时数据库写入、文件 I/O、与第三方 API 的通信、流式传输实时数据等等。

通常,IO 密集型进程的性能取决于服务器的I/O系统,I/O中(如写数据到磁盘)的任何延迟都可能导致系统瓶颈。在I/O密集型进程中,CPU使用率相对较少,它需要等待I/O过程完成才能执行某个进程。

而在CPU密集型进程中,性能主要取决于CPU的速度。系统大部分时间都花在执行 CPU 中的进程上,而不是与外部组件通信。CPU性能越好,系统性能就越好。

如上所述,实时并发应用程序中的关键进程(例如高吞吐量网络操作、数据库写入、组件间通信等)会由于 IO 操作而引入系统延迟。

为了保证低延迟,不同 的Web 框架利用不同的策略(例如非阻塞 IO、单线程架构的异步事件处理、参与者模型、反应式编程等)来实现可扩展的实时服务。

在本文中,将讨论 NodeJS的单线程事件循环模型架构来处理大量 IO 密集型进程。

让我们开始吧。

在深入研究单线程架构之前,让我们先了解一下传统的基于线程请求的模型存在的问题。

基于线程的同步模型的 IO 瓶颈

在传统的基于线程的同步模型中,应用服务器利用该模型来处理客户端请求时,对于 I/O 密集型应用服务,会面临请求吞吐量瓶颈。

以Apache Tomcat服务器为例,它会维护一个线程池,当它接收到客户端请求时,该请求会被分配给线程池中的一个工作线程来处理,详细流程如下:

  • 客户端发送一个请求到Web服务器;
  • Web服务器收到请求后,从线程池中选择一个空闲可用的线程用于处理该请求;
  • 此线程读取客户端请求,处理客户端请求,执行阻塞的IO操作(如果需要)和准备响应;
  • 此线程将准备好的请求发送回Web服务器;
  • Web服务器又将此响应发送到相应的服务器。

服务器为所有客户端执行以上步骤,为每一个客户端请求尽量分配一个线程,如果线程池可用线程数少于并发请求数时,则在使用完所有线程之后,剩余的客户端请求会在队列中等待。

而在I/O密集型应用中,大多数请求都会执行 IO 操作,例如,向数据库发出查询。在这种情况下,只要来自服务器的请求正在等待来自数据库的响应,该工作线程就会暂时被阻塞。它无法处理对服务器的其他请求。

因此如果这些线程中有大量的阻塞IO操作(例如:和数据库、文件系统、外部服务等交互),那么剩余的客户端将会等待很长的时间。

可用看出在高并发流量的 I/O 密集型应用中,这种线程阻塞行为会导致资源争用、并发性降低和性能瓶颈。

解决 IO 瓶颈问题

不同的编程语言和各自生态系统会采用一些异步方法(单线程事件循环模型、Actor模型、响应式)来解决同步请求阻塞问题。本文主要介绍NodeJS的架构和单线程事件循环模型。

NodeJS 从最基本的设计出发,目的就在于通过其单线程事件循环架构,以最小的开销高效处理大量并发请求和异步 IO 操作。作为主线程处理所有客户端请求,并将所有 IO 操作委托给其它线程,详细流程如下。

  • 客户端发送请求到Web服务器;
  • NodeJS的Web服务器在内部维护一个有限的线程池,以便为客户端请求提供服务;
  • NodeJS的Web服务器接收这些请求并将它们放入队列(Event Queue)中。 它被称为“事件队列”
  • NodeJS的Web服务器内部有一个组件,称为“事件循环(Event Loop Single Thread)”,从英文名可以看出,事件循环只使用到了一个线程,使用无限循环来接收请求并处理它们。它是NodeJS的处理模型的核心
  • 事件循环回去检查是否有客户端的请求被放置在事件队列中。如果没有,会一直等待事件队列中存在请求。
  • 如果事件队列中有需要处理的客户端请求,则会从事件队列中选择一个请求。

在事件循环线程处理客户端请求时,根据请求的类型,有不同的处理方式:

  • 如果该客户端请求不需要任何阻塞IO操作,则处理所有内容,准备响应并将其发送回客户端
  • 如果该客户端请求需要一些阻塞IO操作,例如与数据库,文件系统,外部服务交互,就会从从内部线程池获取一个可用的线程并将此客户端请求分配给该线程,这个内部线程池的线程负责接收该请求,处理该请求,执行阻塞IO操作,准备响应并将其发送回事件循环,事件循环依次将响应发送到相应的客户端

以上图为例,Web服务器内部维护着一个有限的线程池,线程池中线程数量为m个,NodeJS的Web服务器接收到Client-1, Client-2, …, Client-n的请求后,将请求放入到事件队列中NodeJS的事件循环从队列中开始拾取这些请求,以Client-1的请求和Client-3为例。

对于Client-1的请求:

  • 事件循环检查Client-1 Request-1是否确实需要任何阻塞IO操作,或者需要更多时间来执行复杂的计算任务
  • 由于此请求是简单计算和非阻塞IO任务,因此不需要单独的线程来处理它
  • 事件循环处理该请求所需要的操作,准备其响应Response-1
  • 事件循环发送Response-1到Client-1

对于Client-3的请求:

  • 检查Client-n Request-n是否需要任何阻塞IO操作或花费更多时间来执行复杂的计算任务
  • 由于此请求有非常复杂的计算或阻塞IO任务,因此事件循环不会处理此请求
  • 事件循环从内部线程池中获取线程T-1,并将此Client-n Request-n分配给线程T-1
  • 线程T-1读取并处理Request-n,执行必要的阻塞IO或计算任务,最后准备响应Response-n
  • 线程T-1将此Response-n发送到事件循环,事件循环依次将此Response-n发送到Client-n

举个生活中的案例,以大排档点餐为例,大排档有已经做好的熟食,此外还可以根据顾客的要求现做,服务员(事件循环)在一个时间段内只能处理一个顾客的点餐请求(事件队列中的任务)。每当一个顾客点完餐,服务员就会检查下菜单,如果顾客点的是熟食(没有阻塞的I/O任务),服务员直接把菜端给客户就行了(直接处理);但是如果顾客点的是现做的食物,那么服务员就选一个空闲的厨师(内部线程池中可用的线程)将顾客的需求转给他,等厨师做好后递给服务员,服务员再端给客户。

为了加深理解,这里有个简单的代码示例:

public class ThreadPoolEventLoop {
    private final Queue<Runnable> eventQueue = new LinkedBlockingQueue<>();
     创建一个固定大小为3的线程池
    private final ExecutorService threadPool = Executors.newFixedThreadPool(3); 

    public void startEventLoop() {
        while (true) {
            Runnable event;
            synchronized (eventQueue) {
                event = eventQueue.poll(); // 从队列中取出事件
            }
            if (event != null) {
                // 判断事件是否是 I/O 阻塞任务
                if (isIOBound(event)) {
                    threadPool.submit(event); // 使用线程池提交 I/O 阻塞任务
                } else {
                    event.run(); // 处理非阻塞任务
                }
            } else {
                try {
                    Thread.sleep(100); // 如果没有事件,等待
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    // 模拟判断事件是否是 I/O 阻塞任务的方法
    private boolean isIOBound(Runnable event) {
        // 这里可以根据具体的业务逻辑判断事件是否是 I/O 阻塞任务
        // 此处简单地假设所有事件都是非阻塞的
        return false;
    }

    public void registerEvent(Runnable event) {
        synchronized (eventQueue) {
            eventQueue.offer(event); // 将事件添加到队列尾部
        }
    }

    public static void main(String[] args) {
        ThreadPoolEventLoop eventLoop = new ThreadPoolEventLoop();

        // 注册几个简单的事件,其中一个模拟 I/O 阻塞任务
        eventLoop.registerEvent(() -> System.out.println("Event 1 executed"));
        eventLoop.registerEvent(() -> System.out.println("Event 2 executed"));
        eventLoop.registerEvent(() -> System.out.println("Event 3 executed"));

        // 启动事件循环
        eventLoop.startEventLoop();
    }
}

//输出
Event 1 executed
Event 2 executed
Event 3 executed

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

泉盛UV-K5扩容2Mbit EEPROM

泉盛UV-K5扩容2Mbit EEPROM 步骤 分离前面板与背板。 拆下电池&#xff0c;底部有个空隙&#xff0c;从缝隙撬开背板。分离前面板时注意喇叭连接线&#xff0c;不要扯断了。 分离屏幕。 先从箭头位置向上挑起&#xff0c;屏幕稍微松动即可左右晃动&#xff0c;直至完全取出。注…

leetcode日记(40)N皇后

一开始没看到不能同斜线&#xff0c;以为只要不同行不同列就行&#xff0c;本来想先列出每一行的Q都不同位置的棋盘然后进行排列组合就行&#xff0c;后来才发现还有限制&#xff08;后来又想了一下&#xff0c;感觉可以先用这种思路然后去除有同一斜线的棋盘摆列&#xff09; …

【手写数据库内核组件】0501多线程并发模型,任务分发多工作者执行架构实现,多线程读写状态时volatile存储类型使用技巧

0501 多线程管理 ​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 文章目录 0501 多…

2024年金航标和萨科微扩张

近年电子信息产业链的外迁和世界经济的低迷&#xff0c;各行各业都很卷&#xff0c;加班加点但业绩负增长是常态&#xff0c;互联网大厂阿里巴巴大裁员、字节跳动裁到了大动脉、京东刘强东抛弃躺平的兄弟、深圳华强北做电子元器件的老板老板娘们一脸茫然&#xff0c;周围都弥漫…

使用工作日志 - 更快地恢复专注并理清思路

原文&#xff1a;Charles Fval - 2024.07.12 你正在处理计算机科学中最复杂的问题&#xff1a;修复部署管道上的权限。这已经是你开始处理这个简单任务的第 4 天了。你的经理明确告诉你&#xff0c;你在这方面的表现远低于她对一个中期实习生的期望。你的同事们都尽量远离你&a…

华为OD 机试真题 - 分割均衡字符串(Python)

题目描述 均衡串定义:字符串只包含两种字符&#xff0c;且两种字符的个数相同。 给定一个均衡字符串&#xff0c;请给出可分割成新的均衡子串的最大个数。 约定字符串中只包含大写的’X"和’Y’两种字符。 输入描述 均衡串:XXYYXY 字符串的长度[2,10000]。给定的字符…

南京邮电大学统计学课程实验2 用EXCEL进行参数估计假设检验 指导

一、实验描述 实验目的 1、学会用Excel进行参数估计&#xff1b; 2、学会用Excel进行z检验-双样本平均差检验&#xff1b; 实验环境 实验中使用以下软件和硬件设备 &#xff08;1&#xff09;Windows XP操作系统&#xff1b; &#xff08;2&#xff09;PC机、EXCEL软件&…

[Vulnhub] digitalworld.local-JOY snmp+ProFTPD权限提升

信息收集 IP AddressOpening Ports192.168.101.150TCP:21,22,25,80,110,139,143,445,465,587,993,995 $ nmap -p- 192.168.101.150 --21,22,25,min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 21/tcp open ftp ProFTPD | ftp-anon: Anonymous FTP logi…

Python 面向对象编程,创建类和对象

面向对象编程&#xff08;Object-Oriented Programming&#xff0c;简称 OOP&#xff09;是一种程序设计范式&#xff0c;旨在提高软件的可维护性、可扩展性和复用性。OOP 的核心思想是将数据和操作这些数据的代码封装在一起&#xff0c;通过类和对象来组织程序&#xff0c;使程…

Windows系统中MySQL的安装和卸载(详细包含msi和zip下载方式,以及完全卸载方法,易出现问题及解决方案等)

MySQL的安装: 第一种:msi安装(交简单,但是不能自定义安装路径) 下载地址:https://dev.mysql.com/downloads/installer/ 选择历史版本 选择安装版本,这里我选择的是8.0.37的版本,然后点击Download下载离线安装包 如下图即为下载好的版本,双击打开安装 出现如下情况,…

vue3中基于dayjs实现日历

import dayjs from dayjs export const useCreateCander () > {let calendarDay []// 当前年&#xff0c;去年&#xff0c;明年let year dayjs().year()let prvYear year - 1let nextYear year 1// 当前月、上月、下月let month dayjs().month() 1let prvMonth mon…

CentOS 7 Web面板的文件管理器说明

在使用CentOS 7 Web Panel&#xff08;CWP7&#xff09;时&#xff0c;偶尔要求在服务器曲面上修改&#xff0c;创建&#xff0c;编辑或删除文件。 最简单&#xff0c;最直接的方式是通过利用CWP7的内置文件管理器。 本文将详细介绍如何启动它&#xff0c;使用它&#xff0c;以…

c++信号和槽机制的轻量级实现,sigslot 库介绍及使用

Qt中的信号与槽机制很好用&#xff0c;然而只在Qt环境中。在现代 C 编程中&#xff0c;对象间的通信是一个核心问题。为了解决这个问题&#xff0c;许多库提供了信号和槽&#xff08;Signals and Slots&#xff09;机制。今天推荐分享一个轻量级的实现&#xff1a;sigslot 库。…

基于LSTM及其变体的回归预测

1 所用模型 代码中用到了以下模型&#xff1a; 1. LSTM&#xff08;Long Short-Term Memory&#xff09;&#xff1a;长短时记忆网络&#xff0c;是一种特殊的RNN&#xff08;循环神经网络&#xff09;&#xff0c;能够解决传统RNN在处理长序列时出现的梯度消失或爆炸的问题。L…

MBR40150FCT-ASEMI无人机专用MBR40150FCT

编辑&#xff1a;ll MBR40150FCT-ASEMI无人机专用MBR40150FCT 型号&#xff1a;MBR40150FCT 品牌&#xff1a;ASEMI 封装&#xff1a;TO-220F 批号&#xff1a;最新 最大平均正向电流&#xff08;IF&#xff09;&#xff1a;40A 最大循环峰值反向电压&#xff08;VRRM&a…

typeorm实体多对多关系指定表名与关联字段

表结构 user 用户表结构 course 课程表结构 user_course 用户课程表 (每个用户可以有多个课程, 每个课程可以有多个用户, 该表用以建立多对多关系) 实体 user.entity.ts Entity(user, { schema: test }) export class User {PrimaryGeneratedColumn({ type: int, name: id }…

江科大SPI教程听课笔记

原理部分我打算听江科大的课复习一下&#xff0c;代码部分工作大概率用HAL库敲了。 SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。 硬件资源方面需要四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO (Master Input Slave…

自定义组件--密码修改对话框(拿来即用型)

前言 一个完整的系统中用户登录功能是不可或缺的&#xff0c;因此用户密码的修改对于前端开发者而言也是工作的重要一环&#xff0c;密码修改分为两种情况&#xff1a;一是用户自身想更换密码&#xff1b;另一种是忘记密码只能选择更换密码。本文自定义了一个通用且常见的组件-…

IDEA快速生成项目树形结构图

下图用的IDEA工具&#xff0c;但我觉得WebStorm 应该也可以 文章目录 进入项目根目录下&#xff0c;进入cmd输入如下指令&#xff1a; 只有文件夹 tree . > list.txt 包括文件夹和文件 tree /f . > list.txt 还可以为相关包路径加上注释

【STM32嵌入式系统设计与开发---拓展】——1_9_1上拉输入和下拉输入

在使用GPIO引脚时&#xff0c;上拉输入和下拉输入的选择取决于外部电路的特性和应用需求。以下是它们各自的应用场景&#xff1a; 1、上拉输入&#xff08;Pull-up Input&#xff09; 用途: 当默认状态需要为高电平时。 避免引脚悬空&#xff08;floating&#xff09;导致的…