控制请求并发数量:p-limit 源码解读
p-limit
是一个控制请求并发数量的库,他的整体代码不多,思路挺好的,很有学习价值;
举例
当我们同时发起多个请求时,一般是这样做的
Promise.all([
requestFn1,
requestFn2,
requestFn3
]).then(res =>{})
或者
requestFn1()
requestFn2()
requestFn3()
而使用 p-limit 限制并发请求数量是这样做的:
var limit = pLimit(8); // 设置最大并发数量为 8
var input = [ // Limit函数包装各个请求
limit(() => fetchSomething('1')),
limit(() => fetchSomething('2')),
limit(() => fetchSomething('3')),
limit(() => fetchSomething('4')),
limit(() => fetchSomething('5')),
limit(() => fetchSomething('6')),
limit(() => fetchSomething('7')),
limit(() => fetchSomething('8')),
];
// 执行请求
Promise.all(input).then(res =>{
console.log(res)
})
上面 input
数组包含了 8
个 limit
函数,每个 limit
函数包含了要发起的请求
当设置最大并发数量为 8
时,上面 8
个请求会同时执行
来看下效果,假设每个请求执行时间为1s
。
var fetchSomething = (str) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(str)
resolve(str)
}, 1000)
})
}
当设置并发请求数量为 2
时
当设置并发请求数量为 3
时
p-limit 限制并发请求数量本质上是,在内部维护了一个请求队列;
当请求发起时,先将请求推入队列,判断当前执行的请求数量是否小于配置的请求并发数量,如果是则执行当前请求,否则等待正在发起的请求中谁请求完了,再从队列首部取出一个执行;
源码(v2.3.0)
pLimit
源码如下(这个源码是 v2.3.0 版本的,因为项目中引入的版本比较早。后面会分析从 2.3.0
到最新版本的源码,看看增加或者改进了什么):
'use strict';
const pTry = require('p-try');
const pLimit = concurrency => {
// 限制为正整数
if (!((Number.isInteger(concurrency) || concurrency === Infinity) && concurrency > 0)) {
return Promise.reject(new TypeError('Expected `concurrency` to be a number from 1 and up'));
}
const queue = []; // 请求队列
let activeCount = 0; // 当前并发的数量
const next = () => { // 一个请求完成时执行的回调
activeCount--;
if (queue.length > 0) {
queue.shift()();
}
};
const run = (fn, resolve, ...args) => { // 请求开始执行
activeCount++;
const result = pTry(fn, ...args);
resolve(result); // 将结果传递给 generator
result.then(next, next); // 请求执行完调用回调
};
// 将请求加入队列
const enqueue = (fn, resolve, ...args) => {
if (activeCount < concurrency) {
run(fn, resolve, ...args);
} else {
queue.push(run.bind(null, fn, resolve, ...args));
}
};
const generator = (fn, ...args) => new Promise(resolve => enqueue(fn, resolve, ...args));
// 暴露内部属性给外界
Object.defineProperties(generator, {
activeCount: {
get: () => activeCount
},
pendingCount: {
get: () => queue.length
},
clearQueue: {
value: () => {
queue.length = 0;
}
}
});
return generator;
};
module.exports = pLimit;
module.exports.default = pLimit;
下面一一剖析下
1、pLimit
函数整体是一个闭包函数,返回了一个名叫 generator
的函数,由 generator
处理并发逻辑,
generator
返回值必须是 promise
,这样才能被 Promise.all
捕获到
const generator = (fn,...args) => new Promise((resolve,reject)=7enqueue(fn,resolve,...args))
2、在 enqueue
函数里面
// 将请求加入队列
const enqueue = (fn, resolve, ...args) => {
if (activeCount < concurrency) {
run(fn, resolve, ...args);
} else {
queue.push(run.bind(null, fn, resolve, ...args));
}
};
activeCount
表示正在执行的请求数量,当 activeCount
小于配置的并发数量(concurrency
)时,则可以执行当前的 fn
(执行 run
函数),否则推入请求队列等待。
3、run
函数接收了三个形参
const run = (fn, resolve, ...args) => { // 请求开始执行
activeCount++;
const result = pTry(fn, ...args);
resolve(result);
result.then(next, next);
};
fn
表示执行的请求,resolve
由generator
定义并往下传,一直跟踪到请求执行完毕后,调用resolve(result);
代表generator
函数fulfilled
···args
表示其余的参数,最终会作为fn
的参数。
4、执行 run
函数时
const run = (fn, resolve, ...args) => { // 请求开始执行
activeCount++; // 请求开始执行,当前请求数量 +1
const result = pTry(fn, ...args);
resolve(result);
result.then(next, next);
};
这里执行 fn
使用的是 const result = pTry(fn,...args)
, pTry
的作用就是创建一个 promise
包裹的结果,不论 fn
是同步函数还是异步函数
// pTry 源码
const pTry = (fn,...args) => new Promise((resolve,reject) => resolve(fn(...args)));
现在 fn
执行(fn(...args)
)完毕并兑现(resolve(fn(...args))
)之后,result
就会兑现。
result
兑现后,generator
的 promise
也就兑现了( resolve(result)
),那么当前请求 fn 的流程就执行完了。
5、当前请求执行完后,对应的当前正在请求的数量也要减一,activeCount--
const next = () => { // 一个请求完成时执行的回调
activeCount--;
if (queue.length > 0) {
queue.shift()();
}
};
然后继续从队列头部取出请求来执行
6、最后暴露内部属性给外界
Object.defineProperties(generator, {
activeCount: { // 当前正在请求的数量
get: () => activeCount
},
pendingCount: { // 等待执行的数量
get: () => queue.length
},
clearQueue: {
value: () => {
queue.length = 0;
}
}
});
源码(v2.3.0)=> 源码(v6.1.0)
从 v2.3.0 到最新的 v6.1.0 版本中间加了一些改进
1、v3.0.0:始终异步执行传进 limit 的函数
在 3.0.0
中,作者将请求入队放在前面,将 if
判断语句和请求执行置于微任务中运行;正如源码注释中解释的:因为当 run
函数执行时,activeCount
是异步更新的,那么这里的 if
判断语句也应该异步执行才能实时获取到 activeCount
的值。
这样一开始批量执行 limit(fn)
时,将会先把这些请求全部放入队列中,然后再根据条件判断是否执行请求;
2、v3.0.2:修复传入的无效并发数引起的错误;
将 return Promise.reject
改为了直接 throw
一个错误
3、v3.1.0:移除 pTry
的依赖;改善性能;
移除了 pTry
依赖,改为了 async
包裹,上面有提到,pTry
是一个 promise
包装函数,返回结果是一个 promise
;两者本质都是一样;
增加了 yocto-queue
依赖,yocto-queue
是一个队列数据结构,用队列代替数组,性能更好;队列的入队和出队操作时间复杂度是 O(1)
,而数组的 shift()
是 O(n)
;
4、v5.0.0:修复上下文传播问题
引入了 AsyncResource
export const AsyncResource = {
bind(fn, _type, thisArg) {
return fn.bind(thisArg);
}
}
这里用 AsyncResource.bind()
包裹 run.bind(undefined, fn, resolve, args)
,其实不是太明白为啥加这一层。。。这里用的到三个参数(fn,resolve,args
)都是通过函数传参过来的,和 this
没关系吧,各位知道的可以告知下么。
相关 issue
在这里
5、6.0.0:性能优化,主要优化的地方在下面
移除了 AsyncResource.bind()
,改为使用一个立即执行的 promise
,并将 promise
的 resolve
方法插入队列,一旦 resolve
完成兑现,调用相应请求;相关 issue
在这里
6、v6.1.0:允许实时修改并发限制数
改变并发数后立马再检测是否可以执行请求;
最后
在上面第4
点的,第5
点中的优化没太看明白,因为执行请求用的到三个参数(fn,resolve,args
)都是通过函数传参过来的,看起来 this
没关系,为啥要进行多层 bind
绑定呢?各位知道的可以不吝赐教下么。
控制请求并发数量:p-limit 源码解读的更多相关文章
- JDK并发基础与部分源码解读
之前写的一个ppt 搬到博客来
- MyBatis源码解读之延迟加载
1. 目的 本文主要解读MyBatis 延迟加载实现原理 2. 延迟加载如何使用 Setting 参数配置 设置参数 描述 有效值 默认值 lazyLoadingEnabled 延迟加载的全局开关.当 ...
- Java并发系列[5]----ReentrantLock源码分析
在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...
- Java高并发程序设计学习笔记(五):JDK并发包(各种同步控制工具的使用、并发容器及典型源码分析(Hashmap等))
转自:https://blog.csdn.net/dataiyangu/article/details/86491786#2__696 1. 各种同步控制工具的使用1.1. ReentrantLock ...
- Alamofire源码解读系列(十二)之请求(Request)
本篇是Alamofire中的请求抽象层的讲解 前言 在Alamofire中,围绕着Request,设计了很多额外的特性,这也恰恰表明,Request是所有请求的基础部分和发起点.这无疑给我们一个Req ...
- Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式
通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...
- Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式
在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...
- Flask(4)- flask请求上下文源码解读、http聊天室单聊/群聊(基于gevent-websocket)
一.flask请求上下文源码解读 通过上篇源码分析,我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__call__方法返回了app的wsgi_app(en ...
- flask的请求上下文源码解读
一.flask请求上下文源码解读 通过上篇源码分析( ---Flask中的CBV和上下文管理--- ),我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__ ...
- Java并发工具类CountDownLatch源码中的例子
Java并发工具类CountDownLatch源码中的例子 实例一 原文描述 /** * <p><b>Sample usage:</b> Here is a pai ...
随机推荐
- Day 8 - 并查集、堆、set 与 map
并查集 引入 并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素. 顾名思义,并查集支持两种操作: 合并(\(\text{Union}\ ...
- 集群及分布式定时任务中间件MEE_TIMED
集群及分布式定时任务中间件MEE_TIMED 转载请著名出处:https://www.cnblogs.com/funnyzpc/p/18312521 MEE_TIMED一套开源的定时任务中间件,MEE ...
- 深入浅出分析最近火热的Mem0个性化AI记忆层
最近Mem0横空出世,官方称之为PA的记忆层,The memory layer for Personalized AI,有好事者还称这个是RAG的替代者,Mem0究竟为何物,背后的原理是什么,我们今天 ...
- mybatis-plus的BaseMapper调用报错:Invalid bound statement
1.yml的配置, 2.@mapper/@mapperScan 3.注意版本依赖冲突,本人第一次使用spring-boot-start-parent3.2.3与mybatis-plus-start-p ...
- tcp udp测试
tcp udp测试 sub_udp.py #!/usr/bin/env python # -*- coding: utf-8 -*- # 可以正常接收udp import socket import ...
- 不止于面向对象的SOLID原则
SOLID原则是由人称"鲍勃大叔"的Rober C. Martin所提出来的.他用五个面向对象设计原则的首字母组成了SOLID,并使其得到了广泛传播.这五个原则罗列如下: 单一指责 ...
- 【Java】Reflection 反射机制 01概述
Reflection 反射机制 反射允许程序在执行期间借助ReflectionAPI获取任何类的内部信息,直接操作任意对象的内部属性和方法 加载完类之后,堆内存的方法区产生了一个Class 类类型的对 ...
- 【转载】 优必选悉尼 AI 研究院何诗怡:基于课程学习的强化多标签图像分类算法 | 分享总结
原文地址: https://baijiahao.baidu.com/s?id=1603057342167437458&wfr=spider&for=pc 来源"雷锋网&quo ...
- python语言绘图:绘制一组以beta分布为先验,以二项分布为似然的贝叶斯后验分布图
代码源自: https://github.com/PacktPublishing/Bayesian-Analysis-with-Python ============================= ...
- 简化数据流:Apache SeaTunnel实现多表同步的高效指南
Apache SeaTunnel除了单表之间的数据同步之外,也支持单表同步到多表,多表同步到单表,以及多表同步到多表,下面简单举例说明如何实现这些功能. 单表 to 单表 一个source,一个sin ...