深入p-limit源码,如何使用p-limit来限制并发数❓

7,097 阅读6分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

hey🖐! 我是pino😊😊。一枚小透明,期待关注➕ 点赞,共同成长~

p-limit是一个限制并发的库。

github地址:github.com/sindresorhu…

并发❓

有两个任务A和B,在一段时间内,通过在A和B两个任务间切换,来完成两个任务,这种情况叫并发。

虽然js是单线程执行的,但是我们却可以同时发起多个异步操作,来起到并发的效果,虽然计算的过程是同步的。

Promise.all

Promise中提供了Promise.all

 const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

 // 生成一个Promise对象的数组
 const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
   return getJSON('/post/' + id + ".json");
 });
 ​
 Promise.all(promises).then(function (posts) {
   // ...
 }).catch(function(reason){
   // ...
 });

Promise.all这个api就赋予了js并发来处理异步任务的能力。但是并发数有时候是需要限制的,因为不可能无限制的很多个异步任务都进行并发处理,多个并发请求可能会对服务端产生压力。而p-limit这个库就是用来限制并发数的。

p-limit

WechatIMG539.png

安装

 npm install p-limit

基本使用

 import pLimit from 'p-limit';
 // 定义并发数量
 const limit = pLimit(1);
 // limit接受一个异步任务
 const input = [
   limit(() => fetchSomething('foo')),
   limit(() => fetchSomething('bar')),
   limit(() => doSomething())
 ];
 ​
 // 使用Promise.all来接收异步任务列表
 const result = await Promise.all(input);

p-limit使用pLimit来定义并发数,返回一个函数用于接收每一个异步任务,保证无论传入多少个异步任务,都始终根据传入的并发数量来进行执行。

源码解析

pLimit

首先来看一下初始化的pLimit函数:

 export default function pLimit(concurrency) {
   // 验证并发数的合法性
   if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
     throw new TypeError('Expected `concurrency` to be a number from 1 and up');
   }
   // 初始化队列
   const queue = new Queue();
   // 初始化计数器
   let activeCount = 0;
   // 返回generator?
   return generator;
 }

可以看到pLimit这个函数主要是判断传入并发值的合法性,初始化activeCount(计数器),初始化queue队列,queue是另外的一个库yocto-queue,实现了一个队列的结构,如果想要深入了解yocto-queue,请移步:

深入yocto-queue源码,60余行代码实现一个链表队列🎉🎉

p-limit中主要用到了yocto-queue的两个方法:enqueue(入队),dequeue(c出队)。 多个 generator 函数会共用一个队列。

generator

generator为初始化pLimit后的入口函数,接收异步函数和剩余参数,调用Promiseresolve传递给enqueue函数。 每个 generator 函数执行会将一个异步函数压入队列。

 const generator = (fn, ...args) => new Promise(resolve => {
   // 调用enqueue函数,传入fn, resolve, args
   // resolve为promise返回的执行成功的函数
   enqueue(fn, resolve, args);
 });

接下来顺藤摸瓜再来看一下enqueue函数做了哪些事情。

enqueue

enqueue主要是用于处理异步函数的入队操作。

 const enqueue = (fn, resolve, args) => {
   // 入队,使用run函数对异步函数fn进行包装
   queue.enqueue(run.bind(undefined, fn, resolve, args));
   // 自执行的async函数
   (async () => {
     await Promise.resolve();
     // 如果当前的计数器(当前执行的任务)小于规定的并发数
     // 并且队列中还有未执行的函数,那么取出函数进行执行
     // queue.dequeue()为出队操作,再次执行为执行异步函数
     if (activeCount < concurrency && queue.size > 0) {
       queue.dequeue()();
     }
   })();
 };

enqueue函数在入队操作的同时又对异步函数使用run函数进行了一层包装,然后执行async函数,判断是否还有未执行的函数,如果队列中还有未执行的函数,那么取出函数进行执行。

但是如果只是在队列中执行出队操作,执行异步函数,那么为什么还要使用async函数进行包裹处理呢?其实源码中也有解释:

This function needs to wait until the next microtask before comparing activeCount to concurrency, because activeCount is updated asynchronously when the run function is dequeued and called. The comparison in the if-statement needs to happen asynchronously as well to get an up-to-date value for activeCount.

这个函数需要等到下一个微任务时再进行比较 activeCount'与concurrency'的比较,因为activeCount'是异步更新的。 当run函数被取消队列并调用时。在if-statement中的比较中的比较也需要异步进行,以获得activeCount`的最新值。

其实就是将异步函数出队执行的操作放入微任务队列,而await Promise.resolve()也只是返回一个成功的状态。以便于获取最新的activeCount的值。

run

在被出队后的异步函数真正执行的是run函数。它在内部控制了计数器的个数,执行resolve函数以及执行下一个异步任务。

 const run = async (fn, resolve, args) => {
   // 执行异步函数,计数器加一
   activeCount++;
   // 执行异步函数,保存执行结果
   const result = (async () => fn(...args))();
   // 执行resolve函数
   resolve(result);
 ​
   try {
     await result;
   } catch {}
   // 取出下一个执行
   next();
 };

为保证 next 的顺序,采用了 await result

next

next函数主要处理此次异步函数执行完毕后的后续操作,包括对计数器减一,取出后续的异步任务。

 const next = () => {
   // 当前执行任务减一
   activeCount--;
   // 如果队列中还有任务,那么继续取出执行
   if (queue.size > 0) {
     queue.dequeue()();
   }
 };

通过函数 enqueuerunnextplimit 就产生了一个限制大小但不断消耗的异步函数队列,从而起到限流的作用。

未命名绘图.drawio.png

其他

p-limit中还定义了一些辅助函数,比如:activeCount(获取当前执行任务的个数)、pendingCount(当前等待任务个数,队列中的个数)、clearQueue(清空队列)

 Object.defineProperties(generator, {
   activeCount: {
     get: () => activeCount,
   },
   pendingCount: {
     get: () => queue.size,
   },
   clearQueue: {
     value: () => {
       queue.clear();
     },
   },
 });

写在最后 ⛳

未来可能会更新实现mini-vue3javascript基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

本文参考

blog.csdn.net/krfwill/art…

mp.weixin.qq.com/s/6LsPMIHdI…

es6.ruanyifeng.com/#docs/promi…