【源码共读】大并发量如何控制并发数

简介: 【源码共读】大并发量如何控制并发数


在常见的网络请求的过程中,我们都会遇到并发的情况,如果一次性发起过多的请求,会导致服务器压力过大,甚至会导致服务器崩溃,所以我们需要控制并发的数量,这样才能保证服务器的正常运行。


今天带来的就是并发控制的库p-limit

使用


根据README的介绍,我们可以通过p-limit来创建一个限制并发的函数,然后通过这个函数来执行我们的异步任务。

import pLimit from 'p-limit';
const limit = pLimit(1);
const input = [
   limit(() => fetchSomething('foo')),
   limit(() => fetchSomething('bar')),
   limit(() => doSomething())
];
// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);

源码分析


我们先来看一下p-limit的源码:

import Queue from 'yocto-queue';
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;
   const next = () => {
      activeCount--;
      if (queue.size > 0) {
         queue.dequeue()();
      }
   };
   const run = async (fn, resolve, args) => {
      activeCount++;
      const result = (async () => fn(...args))();
      resolve(result);
      try {
         await result;
      } catch {}
      next();
   };
   const enqueue = (fn, resolve, args) => {
      queue.enqueue(run.bind(undefined, fn, resolve, args));
      (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`.
         await Promise.resolve();
         if (activeCount < concurrency && queue.size > 0) {
            queue.dequeue()();
         }
      })();
   };
   const generator = (fn, ...args) => new Promise(resolve => {
      enqueue(fn, resolve, args);
   });
   Object.defineProperties(generator, {
      activeCount: {
         get: () => activeCount,
      },
      pendingCount: {
         get: () => queue.size,
      },
      clearQueue: {
         value: () => {
            queue.clear();
         },
      },
   });
   return generator;
}

加上注释和换行只有68行代码,非常简单,我们来一行一行的分析:


可以看到最开始就导入了yocto-queue这个库,这个库之前有分析过:【源码共读】yocto-queue 一个微型队列数据结构


这个库就是一个队列的数据结构,不懂的可以直接将这个理解为数组就好;


跟着使用的代码来看,最开始就是通过pLimit来创建一个限制并发的函数,这个函数接收一个参数concurrency,然后返回一个函数,来看看这一步的代码:

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 generator = (fn, ...args) => new Promise(resolve => {
      enqueue(fn, resolve, args);
   });
   return generator;
}

首先这个函数接收一个参数concurrency,然后判断这个参数是否是一个大于0的整数,如果不是就抛出一个错误;


返回的函数很简单,就是接收一个函数fn和参数args,然后返回一个Promise;


然后调用返回的generator函数就会执行enqueue函数,对应的代码如下:

const enqueue = (fn, resolve, args) => {
    queue.enqueue(run.bind(undefined, fn, resolve, args));
    (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`.
        await Promise.resolve();
        if (activeCount < concurrency && queue.size > 0) {
            queue.dequeue()();
        }
    })();
};

这个函数接收三个参数,fnresolveargs,然后将run函数放入队列中;


这里run使用bind是因为并不需要立即执行,参考:function.prototype.bind()

然后立即执行一个异步函数,这个里面首先会等待下一个微任务,注释解释了这个原因,因为activeCount是异步更新的,所以需要等待下一个微任务才能获取到最新的值;


然后判断activeCount是否小于concurrency,并且队列中有任务,如果满足条件就会将队列中的任务取出来执行,这一步就是并发的核心了;


这里的queue.dequeue()()执行的是run函数,这里容易理解错误,所以框起来。


接下来看看run函数:

const run = async (fn, resolve, args) => {
    activeCount++;
    const result = (async () => fn(...args))();
    resolve(result);
    try {
        await result;
    } catch {
    }
    next();
};

run函数就是用来执行异步并发任务的;


首先activeCount加1,表示当前并发数加1;


然后执行fn函数,这个函数就是我们传入的异步函数,然后将结果赋值给result,注意现在的result是一个处在pending状态的Promise


然后将result传入resolve函数,这个resolve函数就是enqueue函数中返回的Promiseresolve函数;


然后等待result的状态发生改变,这里使用了try...catch,因为result可能会出现异常,所以需要捕获异常;


最后执行next函数,这个函数就是用来处理并发数的,对应的代码如下:

const next = () => {
    activeCount--;
    if (queue.size > 0) {
        queue.dequeue()();
    }
};

首先activeCount减1,表示当前并发数减1;


然后判断队列中是否还有任务,如果有就取出来执行;


queue.dequeue()可以理解为[].shift(),取出队列中的第一个任务,由于确定里面是一个函数,所以直接执行就可以了;


最后面还看到了使用Object.definePropertiesgenerator函数添加了几个属性,来看看:

Object.defineProperties(generator, {
    activeCount: {
        get: () => activeCount,
    },
    pendingCount: {
        get: () => queue.size,
    },
    clearQueue: {
        value: () => {
            queue.clear();
        },
    },
});
  • activeCount:当前并发数
  • pendingCount:队列中的任务数
  • clearQueue:清空队列


这些属性都是只读的,可以让我们在外部知道当前的并发数和队列中的任务数,并且手动清空队列;


动手实现


接下来我们来动手实现一个,抛开队列直接使用数组 + class实现一个简易版:

class PLimit {
    constructor(concurrency) {
        this.concurrency = concurrency;
        this.activeCount = 0;
        this.queue = [];
        return (fn, ...args) => {
            return new Promise(resolve => {
               this.enqueue(fn, resolve, args);
            });
        }
    }
    enqueue(fn, resolve, args) {
        this.queue.push(this.run.bind(this, fn, resolve, args));
        (async () => {
            await Promise.resolve();
            if (this.activeCount < this.concurrency && this.queue.length > 0) {
                this.queue.shift()();
            }
        })();
    }
    async run(fn, resolve, args) {
        this.activeCount++;
        const result = (async () => fn(...args))();
        resolve(result);
        try {
            await result;
        } catch {
        }
        this.next();
    }
    next() {
        this.activeCount--;
        if (this.queue.length > 0) {
            this.queue.shift()();
        }
    }
}

image.png

一共十个并发的任务,每个任务花费 2秒,控制并发数为 2 时,一共花费 10秒。


总结


这篇文章主要介绍了Promise的并发控制,主要是通过队列来实现的。


并发控制的核心就是控制并发数,所以我们需要一个队列来存储任务,然后控制并发数,当并发数小于最大并发数时,就从队列中取出任务执行,这样就可以控制并发数了。


等待上一个任务的执行通过await来实现,这样就可以保证每次都只有可控的并发数在执行。


代码量并不多,但是内部的操作还有细节处理都是知识点。


目录
相关文章
|
7月前
|
Web App开发 JSON 监控
模拟一次超过 5 万的并发用户
模拟一次超过 5 万的并发用户
55 0
|
4月前
|
JavaScript 前端开发 Java
面试官:假如有几十个请求,如何去控制并发?
面试官:假如有几十个请求,如何去控制并发?
|
5月前
|
机器学习/深度学习 Java 数据挖掘
线程操纵术之更优雅的并行策略问题之并发和并行有区别问题如何解决
线程操纵术之更优雅的并行策略问题之并发和并行有区别问题如何解决
|
7月前
|
存储 前端开发 JavaScript
前端面试:如何实现并发请求数量控制?
前端面试:如何实现并发请求数量控制?
260 0
|
数据可视化 测试技术
JMeter 中如何准确设置并发量
JMeter 是一个功能强大的性能测试工具,可以模拟许多用户同时访问应用程序的情况。在使用 JMeter 进行性能测试时,设置并发是非常重要的。本文将介绍如何在 JMeter 中设置并发和查看报告。
JMeter 中如何准确设置并发量
|
测试技术
TPS、并发数与线程数,傻傻分不清楚?
TPS、并发数与线程数,傻傻分不清楚?
557 0
TPS、并发数与线程数,傻傻分不清楚?
|
Java 应用服务中间件 数据库
面试官问:你们服务最大的并发量是多少?
面试官问:你们服务最大的并发量是多少?
530 1
面试官问:你们服务最大的并发量是多少?
|
IDE 物联网 测试技术
项目实战:Qt并发服务器通讯,受同一时刻最大线程数限制(笔者本本同一时刻600多)
项目实战:Qt并发服务器通讯,受同一时刻最大线程数限制(笔者本本同一时刻600多)
项目实战:Qt并发服务器通讯,受同一时刻最大线程数限制(笔者本本同一时刻600多)
|
监控 Java 应用服务中间件
系统gc后线程数增加原因分析过程
问题&现象1、由于系统过一段时间(四五天)commited old区会增大,我们应用中增加每天凌晨一次主动fullgc的任务,但是观察下来发现每天经过system.gc后线程数会增加几个,一直增加到接近300不会增加,并且增加的线程为守护线程。监控图如下:2、某些机器偶然出现线程数陡增情况:分析第一反应为fullgc时会新建gc线程去处理,但是通过jstack指令监控两天的线程变化发现,g
|
测试技术
【高并发】高并发场景下创建多少线程才合适?一条公式帮你搞定!!
创建多少线程合适, 要看多线程具体的应用场景。一般来说,我们可以将程序分为:CPU密集型程序和I/O密集型程序, 而针对于CPU密集型程序和I/O密集型程序,其计算最佳线程数的方法是不同的 。
394 0

相关实验场景

更多