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 数组包含了 8limit 函数,每个 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 表示执行的请求,

  • resolvegenerator 定义并往下传,一直跟踪到请求执行完毕后,调用 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 兑现后,generatorpromise 也就兑现了( 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,并将 promiseresolve 方法插入队列,一旦 resolve 完成兑现,调用相应请求;相关 issue这里

6、v6.1.0:允许实时修改并发限制数

改变并发数后立马再检测是否可以执行请求;


最后

在上面第4点的,第5点中的优化没太看明白,因为执行请求用的到三个参数(fn,resolve,args)都是通过函数传参过来的,看起来 this 没关系,为啥要进行多层 bind 绑定呢?各位知道的可以不吝赐教下么。

控制请求并发数量:p-limit 源码解读的更多相关文章

  1. JDK并发基础与部分源码解读

    之前写的一个ppt 搬到博客来

  2. MyBatis源码解读之延迟加载

    1. 目的 本文主要解读MyBatis 延迟加载实现原理 2. 延迟加载如何使用 Setting 参数配置 设置参数 描述 有效值 默认值 lazyLoadingEnabled 延迟加载的全局开关.当 ...

  3. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  4. Java高并发程序设计学习笔记(五):JDK并发包(各种同步控制工具的使用、并发容器及典型源码分析(Hashmap等))

    转自:https://blog.csdn.net/dataiyangu/article/details/86491786#2__696 1. 各种同步控制工具的使用1.1. ReentrantLock ...

  5. Alamofire源码解读系列(十二)之请求(Request)

    本篇是Alamofire中的请求抽象层的讲解 前言 在Alamofire中,围绕着Request,设计了很多额外的特性,这也恰恰表明,Request是所有请求的基础部分和发起点.这无疑给我们一个Req ...

  6. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

  7. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  8. Flask(4)- flask请求上下文源码解读、http聊天室单聊/群聊(基于gevent-websocket)

    一.flask请求上下文源码解读 通过上篇源码分析,我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__call__方法返回了app的wsgi_app(en ...

  9. flask的请求上下文源码解读

    一.flask请求上下文源码解读 通过上篇源码分析( ---Flask中的CBV和上下文管理--- ),我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__ ...

  10. Java并发工具类CountDownLatch源码中的例子

    Java并发工具类CountDownLatch源码中的例子 实例一 原文描述 /** * <p><b>Sample usage:</b> Here is a pai ...

随机推荐

  1. CF941

    A link 其实,只要有第一次,那么下次随意找一个队列里有的数加\(k-1\)个进去,加上队列里那一个删掉\(k\)个,到最后一次肯定是剩\(k-1\)个. 没有第一次,就是\(n\). 点击查看代 ...

  2. Spring Boot快速入门(二)搭建javaWeb项目

    1.配置pom.xml 教程一创建的项目为maven项目,所以搭建一个Spring Boot的Web项目,先导入一下jar包:即在pom.xml以下依赖: 1 <dependencies> ...

  3. app备案证明需要提供md5值和公钥的解决方案

    现在app上架华为市场.小米市场.苹果市场等大型的应用商店,都需要提供国内的app备案证明.无论是安卓还是ios,都需要备案了. 但是问题是备案的时候需要填写app的bundle ID.公钥和MD5值 ...

  4. Jmeter函数助手1-CSVRead

    CSVRead函数适用于读取文件获取参数值. 用于获取值的CSV文件 | *别名:csv文件路径 CSV文件列号| next| *alias:读取列,0表示第一列,1表示第二列 1.首先我们需要一个文 ...

  5. 【转载】 深入理解TensorFlow中的tf.metrics算子

    原文地址: https://mp.weixin.qq.com/s/8I5Nvw4t2jT1NR9vIYT5XA ============================================ ...

  6. 决定了,今日起开始准备弃用京东JD

    估计京东是为了节约开支,然后开始大比例的把快递物流业务进行外包了,这直接导致服务质量的直线下滑,10多年前我选择弃用当当网而选择京东JD就是因为当时当地的当当网快递是用沈阳晚报的快递上门的,快递员连P ...

  7. Deepin20系统开机报错——You are in emergency mode ... Cannot open access to console, the root account is locked. emergency mode/“journalctl -xb”

    参考: https://knowledge.ipason.com/ipKnowledge/knowledgedetail.html/1286 https://blog.csdn.net/wenfei1 ...

  8. 关于vue按需引入ElMessage和ElMessageBox未被自动引入到auto-important的问题

    相信关于按需引入大家应该都会了,不论是官网还是百度一大堆教程 我这边也是参照https://github.com/youlaitech/vue3-element-admin的写法去写的-----需要的 ...

  9. Sentry 产品指南文档(附:详细脑图整理)

    Sentry 基础知识 https://docs.sentry.io/product/ https://docs.sentry.io/product/sentry-basics/ 问题 https:/ ...

  10. [天线原理及设计>基本原理] 2. 细线天线上的电流分配

    2. 细线天线上的电流分配 为了说明线性偶极子上电流分布的产生及其随后的辐射,让我们首先从无损双线传输线的几何形状开始,如图1.15(a)所示. 电荷的运动沿每条导线产生幅度为I0/2的行波电流.当电 ...