60 秒回答模板

并发池不是串行 await,也不是把所有任务一次性 Promise.all。实现时维护 nextIndex、running、finished 和 results。启动阶段先发起不超过 limit 个任务;每个任务结束后把结果写回原始下标,running 减一、finished 加一,再继续调度下一个未开始任务。全部完成后 resolve。失败策略要提前约定:fail-fast 就遇到第一个失败立即 reject;all-settled 就把成功和失败都记录下来,等所有任务结束后返回。还要处理 limit 小于 1、空任务、同步抛错和任务函数返回非 Promise 的情况。

考点 先定义任务契约
主线 调度状态怎么维护
易错点 传入已经开始执行的 Promise,导致并发限制形同虚…

深入解析

01

先定义任务契约

输入最好是任务函数数组,而不是已经创建好的 Promise。已经创建的 Promise 会立刻开始执行,无法真正限制并发。任务函数在被调度时才执行,返回值用 Promise.resolve 包一层统一处理。

02

调度状态怎么维护

nextIndex 表示下一个待启动任务,running 表示当前运行数,finished 表示已结束任务数,results 按输入长度预分配。只要 running < limit 且还有任务,就继续启动任务。

03

完成后补位

每个任务完成后必须释放一个运行槽位,再调用调度函数补上新任务。这样可以保证任意时刻并发数不超过 limit,同时队列会持续推进到所有任务结束。

04

结果顺序和完成顺序分离

并发任务完成顺序不可控,但返回结果通常要和输入顺序一致。所以不能 results.push(value),而要用任务启动时保存的 index 写回 results[index]。

05

失败策略要说清

fail-fast 模式适合任何一个失败都无法继续的场景,第一次 reject 后整体失败;all-settled 模式适合批量请求或局部容错,记录 fulfilled/rejected 状态并等待全部结束。

06

边界和工程细节

limit 要校验为正整数,空数组直接 resolve。同步异常要转成 rejected。fail-fast 后最好停止继续启动新任务,但已经运行中的任务无法真正取消,除非任务本身支持 AbortController。

javascript

Promise 并发池

function runPool(tasks, limit, { settled = false } = {}) {
  if (!Number.isInteger(limit) || limit < 1) {
    return Promise.reject(new Error("limit must be a positive integer"));
  }

  return new Promise((resolve, reject) => {
    const results = Array(tasks.length);
    let nextIndex = 0;
    let running = 0;
    let finished = 0;
    let stopped = false;

    if (tasks.length === 0) resolve(results);

    function launchMore() {
      if (stopped) return;

      while (running < limit && nextIndex < tasks.length) {
        const index = nextIndex++;
        running++;

        Promise.resolve()
          .then(() => tasks[index]())
          .then(
            (value) => {
              results[index] = settled
                ? { status: "fulfilled", value }
                : value;
            },
            (reason) => {
              if (!settled) {
                stopped = true;
                reject(reason);
                return;
              }
              results[index] = { status: "rejected", reason };
            },
          )
          .finally(() => {
            running--;
            finished++;
            if (finished === tasks.length) resolve(results);
            else launchMore();
          });
      }
    }

    launchMore();
  });
}

易错点

  • 传入已经开始执行的 Promise,导致并发限制形同虚设。
  • 每个任务都 await 后再启动下一个,把并发池写成串行执行。
  • 用 push 收集结果,破坏输入顺序和结果下标对应关系。
  • 没有定义失败策略,导致 reject 后仍继续启动新任务或永远不 resolve。

面试官追问

为什么不能直接传 Promise 数组?

Promise 创建后通常已经开始执行,调度器拿到时无法阻止它们并发。要传任务函数,让并发池决定何时调用。

结果为什么不能直接 push?

push 得到的是完成顺序,不是输入顺序。批量请求通常需要结果与任务一一对应,所以要按启动时的 index 写回。

fail-fast 后正在执行的任务怎么办?

普通 Promise 不能被外部取消。可以停止启动新任务,但已经执行的任务只能等待结束;如果要取消,需要任务内部支持 AbortController 或自定义取消协议。

并发池和节流有什么区别?

并发池限制同时运行的任务数量,关注队列调度;节流限制单位时间触发频率,关注事件触发频率。两者解决的问题不同。