提高nodejs中promise的性能

提高nodejs中promise的性能

我们先来看一个常见问题,假设我们有 N 条记录需要处理,或者例如,为每条记录发出 API 请求以获取数据。

通常情况下我们都是使用promise.all方法来实现这一需求:

// 记录
const data = [{}, {}, {}];// 处理所有的数据(获取所有记录的数据)
const results = await Promise.all(data.map(processRecord)); 

所以这里发生的是我们正在“并行”执行异步代码,所以它在时间线上可能看起来像这样。

在这里插入图片描述

可能我们都知道promise.all方法就是当最后一个promise也解决完才会有结果。 正如上图所看到的,Promise.all()的整体运行时间与批次中最慢的一样长。所以我们的主线程基本上是“什么都不做”,正在等待最慢的请求完成。

为了解决这个问题,本文将讲解如何使用更好的优化这个方法。

promise pool

Promise Pool 的想法是充分利用 Node.js 的主线程的潜力。为了实现更好的利用率,我们需要密集地打包 API 调用(或任何其他异步任务),这样我们就不会等待最长时间的调用完成,而是在第一个调用完成后立即安排下一个调用。

在这里插入图片描述
通过这种方式,我们可以实现多种目标:

  • 通过设置Promise Pool的并发数来控制我们服务的吞吐量;
  • 通过设置Promise Pool的并发度来管理下游服务的负载;
  • 提高我们应用程序的性能;减少 CPU 的空闲时间。

实现

让我们从需求开始。我们希望能够为 Promise Pool 提供一些记录和并发“请求”的最大数量以及可以进行处理的回调。处理后,我们希望收到每个单独记录的处理结果数组。

类似下面的方式:

const results = await PromisePool.for([]).withConcurrency(50).process(async (data) => {return result;});

我们需要跟踪当前正在处理的记录数、总共处理了多少条记录以及其他参数。

const { EventEmitter } = require("events");
class PromisePool {constructor(data,concurrency,processor,) {this.data = data;this.results = [];this.concurrency = concurrency;this.inFlightTasks = 0;this.processedEntries = 0;this.processor = processor;this.eventsEmitter = new EventEmitter();this.executionPromise = null;}
}

让我们添加一个结构方法以及公共函数来配置并发并指定处理器回调。

static for(data) {return new this(data, 10, () => {})}withConcurrency(concurrency) {this.concurrency = concurrency;return this;}process(processor) {this.processor = processor;return this._processRecords();}

我们已经建立了基础,现在我们需要实现 Promise Pool 的关键。这个想法很简单:

我们迭代数组中的每个记录并等待可用的坐位(基本上是我们拥有的并发坐位数量中的空闲坐位),如果我们有空闲坐位,我们将继续安排以下作业,除非所有坐位都被占用。如果所有座位都被占用,我们会等待至少一项工作完成。

 _waitAvailableSit() {if (this.inFlightTasks >= this.concurrency) {return new Promise((res) => {this.eventsEmitter.once(PromisePool.TASK_COMPLETED, res);});} else {return Promise.resolve();}}async _processRecord(data) {try {this.inFlightTasks++;const result = await this.processor(data, this);this.results.push(result);} catch (e) {} finally {this.inFlightTasks--;this.processedEntries++;this.eventsEmitter.emit(PromisePool.TASK_COMPLETED);if (this.inFlightTasks === 0 && this.processedEntries === this.data.length) {this.eventsEmitter.emit(PromisePool.DRAIN)}}}_processRecords() {if (this.executionPromise !== null) {return this.executionPromise;}this.executionPromise = new Promise(async (res, rej) => {try {for (const element of this.data) {await this._waitAvailableSit();this._processRecord(element);}this.eventsEmitter.once(PromisePool.DRAIN, () => res(this.results));} catch (e) {rej(e)}});return this.executionPromise;}

那么,一个可以处理并发“请求”的promise pool就写好了。

性能测试

我们将把处理器模拟为需要 150 到 1150 毫秒执行的函数。

const processor = (i) => {return new Promise((res, rej) => {console.log(`[${Date.now()}] 开始处理 item. i=${i}`);setTimeout(() => {console.log(`[${Date.now()}] 开始处理 item. i=${i}`);res(i);}, 150 + Math.random() * 1000);});
}

然后我们将同时处理 1000 条记录的数组,分成 50 条记录的块。首先,我们使用 Promise.all 方法对其进行测试。

async function main() {const timeStart = Date.now();const data = Array.from({ length: 1000 }).map((d,i) => i);while (data.length > 0) {const arr = data.splice(0, 50);await Promise.all(arr.map(processor));}const timeEnd = Date.now();console.log(`Promise.all=${timeEnd-timeStart}`);
}

然后是使用刚刚写的PromisePool

async function main() {const timeStart = Date.now();const data = Array.from({ length: 1000 }).map((d,i) => i);const promisePool = PromisePool.for(data).withConcurrency(50);await promisePool.process(processor);const timeEnd = Date.now();console.log(`PromisePool=${timeEnd-timeStart}`);
}

在这里插入图片描述

经过测试,使用 Promise.all 处理所有记录平均需要 22.9 秒,而 PromisePool 需要 12.9 秒,快了大约 40%!(大家可以代码复制下来去跑一下)

Promise Pool是同时处理多个记录的绝佳模式,它不仅可以帮助我们提高应用程序的性能,还可以让我们控制下游服务的速率限制,如果大家感兴趣可以自行扩展一下。

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

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

相关文章

枚举和反射

枚举 枚举 枚举是一种特殊的类,它可以有自己的属性、方法和构造方法。 两种枚举的方法 自定义枚举 a.将构造器私有化,防止外部直接new b.去掉set方法,防止属性被修改 c.在内部直接创建固定的对象 通过类名直接去访问 关键字枚举 用…

vue js 回调函数 异步处理 为什么要 let that = this

1 异步就是开个事务(只有主线程 等主线程空闲),用that 值 做处理,然后返回处理结果,而that的值是开启事务那一刻的this的值.而在主线程处理的时候,this的一直在变化, that的值保留在那一刻 ps 或是将本obj 传递给其他的obj使用处理 ps 开启新事务或开启新子线程都是 在新的ob…

Repo manifests默认default.xml清单文件中的各个标签详解

Repo简介 “Repo” 是一个用于管理多个Git存储库的工具,通常与Google的Android开发项目一起使用。它允许您在一个命令下轻松地进行多个Git存储库的同步、下载和管理。 repo下载安装 从清华镜像源下载 mkdir ~/bin PATH~/bin:$PATH curl https://mirrors.tun…

C++ 新特性 | C++ 11 | decltype 关键字

一、decltype 关键字 1、介绍 decltype 是 C11 新增的一个用来推导表达式类型的关键字。和 auto 的功能一样,用来在编译时期进行自动类型推导。引入 decltype 是因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来很不方便&…

2023.8各大浏览器11家对比:Edge/Chrome/Opera/Firefox/Tor/Vivaldi/Brave,安全性,速度,体积,内存占用

测试环境:全默认设置的情况下,均在全新的系统上进行测试,系统并未进行任何改动,没有杀毒软件,浏览器进程全部在后台,且为小窗模式,小窗分辨率均为浏览器厂商默认缩放大小(变量不唯一)&#xff0…

ARM linux ALSA 音频驱动开发方法

+他V hezkz17进数字音频系统研究开发交流答疑群(课题组) 一 linux ALSA介绍 ALSA (Advanced Linux Sound Architecture) 是一个用于提供音频功能的开源软件框架。它是Linux操作系统中音频驱动程序和用户空间应用程序之间的接口。ALSA 提供了访问声卡硬件的低级别API,并支持…

DevExpress WinForms数据编辑器组件,提供丰富的数据输入样式!(二)

DevExpress WinForms超过80个高影响力的WinForms编辑器和多用途控件,从屏蔽数据输入和内置数据验证到HTML格式化,DevExpress数据编辑库提供了无与伦比的数据编辑选项,包括用于独立数据编辑或用于容器控件(如Grid, TreeList和Ribbon)的单元格。…

nginx会话保持

ip_hash:通过IP保持会话 作用: nginx通过后端服务器地址将请求定向的转发到服务器上。 将客户端的IP地址通过哈希算法加密成一个数值 如果后端有多个服务器,第一次请求到服务器A, 并在务器登录成功,那么再登录B服务器就要重新…

AIGC ChatGPT 制作地图可视化分析

地图可视化分析是一种将数据通过地图的形式进行展示的方法,可以让人们更加直观、快速、准确的理解和分析数据。以下是地图可视化分析的一些主要好处: 加强数据理解:地图可视化可以将抽象的数字转化为直观的图形,帮助我们更好地理解…

黑马头条-kafka配置

生产者配置 NAMEDESCRIPTIONTYPEDEFAULTVALID VALUESIMPORTANCEbootstrap.servershost/port列表,用于初始化建立和Kafka集群的连接。列表格式为host1:port1,host2:port2,…,无需添加所有的集群地址,kafka会根据提供的地址发现其他的地址&…

科技赋能,教育革新——大步迈向体育强国梦

在 "全民健身"、"体育强国建设"战略的推进下,体育考试成绩被纳入重要升学考试且分值不断提高,体育科目的地位逐步上升到前所未有的高度,在此趋势下,体育教学正演变出更多元化、个性化的需求。然而现实中却面临…

『C语言入门』探索C语言函数

文章目录 导言一、函数概述定义与作用重要性 二、函数分类库函数自定义函数定义使用好处 三、函数参数实际参数(实参)形式参数(形参)内存分配 四、函数调用传值调用传址调用 五、函数嵌套调用与链式访问嵌套调用链式访问 六、函数…

8.8 【C语言】动态内存分配与指向它的指针变量

8.8.1 什么是内存的动态分配 栈:全局变量和局部变量,全局变量是分配在内存中的静态存储区的,非静态的局部变量是分配在内存中的动态存储区的。 堆:数据临时存放在一个特别的自由存储区。 8.8.2 怎样建立内存的动态分配 对内存…

Python标准库概览

Python标准库概览 知识点 标准库: turtle库(必选)标准库: random库(必选)、time库(可选) 知识导图 1、turtle库概述 turtle(海龟)是Python重要的标准库之一,它能够进行基本的图形绘制。turtle库绘制图形有一个基本框架&#x…

RabbitMQ特性介绍和使用案例

❤ 作者主页:李奕赫揍小邰的博客 ❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~* 🍊 记得点赞、收藏、评论⭐️⭐️⭐️ 📣 认真学习!!!🎉🎉 文章目录 RabbitMQ特性…

Web 开发 Django 管理工具

上次为大家介绍了 Django 的模型,通过模型就可以操作数据库,从而就可以改变页面的展示内容,那问题来了,我们只能通过手动编辑模型文件来配置模型吗?当然不是,Django 为我们提供了强大的工具,可以…

【架构】探索计算机处理器的世界:ARM和x86架构解析及指令集

目录 导语ARM架构x86架构AMD公司对比与应用不同架构处理器的指令集结语 导语 计算机处理器是数字化时代的核心引擎,而在众多处理器架构中,ARM和x86是备受关注的三个。本文将带您深入探索这三个架构,介绍它们的特点、公司背景以及应用领域。让…

ARM Linux 系统稳定性分析入门及渐进 13 -- gdb 反汇编 disassemble 命令详细介绍及举例】

文章目录 1.1 gdb 调试回顾1.1.1 gdb list 命令介绍 1.2 反汇编命令 dis 介绍1.2.1 如何设置 gdb 汇编代码的格式 1.1 gdb 调试回顾 在GNU调试器(GDB)中,有许多命令可以帮助我们调试应用程序。 gdb: 这是一个强大的Unix下的程序调试工具。以…

融资融券利率是多少?最低是哪一家?

按目前市场上统计的数据看,融资融券的默认利率是8.35%,普遍利率在6左右,融资融券简单的来说就是信用账户,包括融资和融券两部分。 融资就是向券商借钱炒股交易,现金融资比例是1:1。股票有折算率&#xff0c…

Java中常见的异常类

在Java中,异常(Exception)是指在程序执行过程中可能出现的错误或异常情况。Java通过异常类来表示这些异常情况,异常类是从java.lang.Exception类继承的。异常类可以分为两大类:Checked异常和Unchecked异常。 Checked异…