← 中文资源

简体中文版本

保持结果顺序的 Promise 并发池

以固定并发度执行大量异步任务,结果严格保持输入顺序,并校验并发参数。

阅读英文原文

功能说明

该示例解决直接使用 Promise.all 时无法限制并发的问题。mapPool 先校验数组、映射函数与并发度,然后启动固定数量的 worker,每个 worker 循环领取下一个索引,调用 mapper 并把结果写回同一下标。无论哪个任务先完成,返回数组的顺序都与输入一致,适合批量请求接口、处理文件列表或构建索引等需要控制资源占用的场景。

关键代码

共享的 nextIndex 在每次 await 之前同步自增,因此多个 worker 不会拿到同一个下标;这是 JavaScript 单线程事件循环下实现工作窃取的关键。worker 数量取并发度与数组长度的较小值,空数组不会启动任何任务。Promise.all 只等待 worker 循环结束,不直接等待业务任务,因此任一 mapper 抛错都会让整个池拒绝,其余已启动任务仍会继续执行到结束。

使用注意

该实现采用快速失败语义,若需要收集每个任务的成功与失败,应改为 allSettled 或在 mapper 内部捕获错误。并发度只限制同时在执行的任务数,不限制排队任务的内存占用,超大数组应改用流式分批。任务抛错后池不会自动取消其他任务,调用方需自行使用 AbortSignal 协调。返回值是全新数组,不会修改输入。

验证方式

用可控的延迟任务记录 active 计数,断言任意时刻 active 不超过设定并发度;用不同延迟打乱完成顺序,验证结果仍按输入下标排列;测试空数组返回空数组、并发度为零或小数抛 RangeError、mapper 非函数抛 TypeError,以及某个任务拒绝时整体 Promise 拒绝。

代码示例

export function mapPool(items, mapper, concurrency = 4) {
  if (!Array.isArray(items)) throw new TypeError("items must be an array");
  if (typeof mapper !== "function") throw new TypeError("mapper must be a function");
  if (!Number.isInteger(concurrency) || concurrency < 1) {
    throw new RangeError("concurrency must be a positive integer");
  }

  const results = new Array(items.length);
  let nextIndex = 0;

  async function worker() {
    while (nextIndex < items.length) {
      const index = nextIndex;
      nextIndex += 1;
      results[index] = await mapper(items[index], index);
    }
  }

  const workerCount = Math.min(concurrency, items.length);
  const workers = Array.from({ length: workerCount }, () => worker());
  return Promise.all(workers).then(() => results);
}