js异步并发控制,限制请求数量的解惑

6,904 阅读3分钟

前言

在一些场景中,我们会遇到高频率大批量请求数据,密集型 CPU 运算。为了避免请求过于频繁导致资源不足情况。 需要保持并行请求的数量固定。

方案

  • 基于Promise.race的特性配合Promise.all 实现并行请求的限制请求数量,保持请求数量。
  • 基于队列先进先出的特性配合Promise 构成微任务异步队列,实现并行请求的限制请求数量,保持请求数量。

基于Promise.race配合Promise.all 实现异步并发限制

运用js的事件循环机制 ,进行异步处理,以下是思路步骤:

  1. 创建异步函数,接受三个参数(限制数量、数据数组、处理函数);
  2. 函数内部执行处理逻辑;
  3. 初始化结果数组、运行执行数组变量 ;
  4. 循环数据数组,包裹处理函数为Promise对象
  5. 添加Promise对象到结果数组(之后的Promise对象执行结果仍保存在结果数组中);
  6. 判断数据数组的长度是否小于等于限制数量;
    1. 如果是继续限量执行;
    2. 在当前的Promise对象其后添加删除自身的处理逻辑(清除执行数组);
    3. 将当前的Promise对象添加到执行数组中;
    4. 判断执行数组长度是否大于等于限制数量;
      1. 为真,则运行Promise.race(执行数组),进入微任务队列;
      2. 为假,跳过;
    5. 不是则跳过;
  7. 使用Promise.all全量执行结果数组并返回结果;
  8. 返回运行结果;

实现代码:

const asyncPool = async (poolLimit, array, iteratorFn) => {
  const resultList = [];
  const executing = [];
  for (const item of array) {
    console.log("循环", item);
    const p = Promise.resolve().then(() => {
     console.log("初始化", item);
      return iteratorFn(item, array);
    });
    resultList.push(p);
    if (poolLimit <= array.length) {
      const e = p.then(() => {
        return executing.splice(executing.indexOf(e), 1);
      });
      executing.push(e);
      if (executing.length >= poolLimit) {
       console.log("运行Promise.race");
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(resultList);
};

示例:

const timeout = (i) =>
  new Promise( (resolve) => {
    setTimeout(resolve, i, i)
  });

const main = async () => {
  const aa = await asyncPool(
    3,
    [10, 20, 30, 40, 50, 60, 60, 70, 80, 1000,],
    timeout
  );
  console.log("aa=>", aa);
};

main();

运行结果:

image.png

如运行结果所示,函数运行,一直保持在限制运行数量为3。

基于队列配合Promise实现异步并发限制

基本的逻辑是,有一个任务池,通过初始化任务池,设置异步并发的数量。通过向任务池添加异步任务,利用Promise.all全部执行完再返回结果的特性,将异步任务收集起来组合成数组传给Promise.all。执行函数并返回结果。

流程图如下:

无标题-2021-09-24-1753.png

实现代码:

// 队列
class Queue {
  constructor() {
    this._queue = [];
  }
  push(value) {
    return this._queue.push(value);
  }
  shift() {
    // TODO 优化出队操作 shift 操作的时间复杂度为 O(n)。使用 reverse + pop 的方式,引入双数组的设计,减低时间复杂度类O(1)
    return this._queue.shift();
  }
  isEmpty() {
    return this._queue.length === 0;
  }
}

// 延迟任务
class DelayedTask {
  constructor(resolve, fn, args) {
    this.resolve = resolve;
    this.fn = fn;
    this.args = args;
  }
}

// 任务池
class TaskPool {
  constructor(size) {
    this.size = size;
    this.queue = new Queue();
  }

  addTask(fn) {
    return (...args) => {
      return new Promise((resolve) => {
        this.queue.push(new DelayedTask(resolve, fn, args));
        if (this.size) {
          this.size--;
          const {
            resolve: taskResolve,
            fn: taskFn,
            args: taskArgs,
          } = this.queue.shift();
          taskResolve(this.runTask(taskFn, taskArgs));
        }
      });
    };
  }
  
  pullTask() {
    if (this.queue.isEmpty()) {
      return;
    }
    if (this.size === 0) {
      return;
    }
    this.size--;
    const { resolve, fn, args } = this.queue.shift();
    resolve(this.runTask(fn, args));
  }

  runTask(fn, args) {
    const result = Promise.resolve(fn(...args));
    result.finally(() => {
      this.size++;
      this.pullTask();
    });
    return result;
  }
}

示例:

const cc = new TaskPool(4);
const taskList = [1000, 3000, 200, 1300, 800, 2000];

const task = (timeout) =>
  new Promise((resolve) =>
    setTimeout(() => {
      resolve(timeout);
    }, timeout)
  );

async function startConcurrentControl() {
  console.time("xxx");
  await Promise.all(taskList.map(cc.addTask(task)));
  console.timeEnd("xxx");
}
startConcurrentControl();

参考

利用 JavaScript 实现并发控制

async-pool