60 秒回答模板

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

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

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