真实面经题目 · 原创解析
手写:并发池
这题考察 Promise 调度能力。核心是限制同时运行的任务数,保持结果顺序,明确失败策略,并在任务完成后持续补位。
真实面经题目 · 原创解析
这题考察 Promise 调度能力。核心是限制同时运行的任务数,保持结果顺序,明确失败策略,并在任务完成后持续补位。
并发池不是串行 await,也不是把所有任务一次性 Promise.all。实现时维护 nextIndex、running、finished 和 results。启动阶段先发起不超过 limit 个任务;每个任务结束后把结果写回原始下标,running 减一、finished 加一,再继续调度下一个未开始任务。全部完成后 resolve。失败策略要提前约定:fail-fast 就遇到第一个失败立即 reject;all-settled 就把成功和失败都记录下来,等所有任务结束后返回。还要处理 limit 小于 1、空任务、同步抛错和任务函数返回非 Promise 的情况。
输入最好是任务函数数组,而不是已经创建好的 Promise。已经创建的 Promise 会立刻开始执行,无法真正限制并发。任务函数在被调度时才执行,返回值用 Promise.resolve 包一层统一处理。
nextIndex 表示下一个待启动任务,running 表示当前运行数,finished 表示已结束任务数,results 按输入长度预分配。只要 running < limit 且还有任务,就继续启动任务。
每个任务完成后必须释放一个运行槽位,再调用调度函数补上新任务。这样可以保证任意时刻并发数不超过 limit,同时队列会持续推进到所有任务结束。
并发任务完成顺序不可控,但返回结果通常要和输入顺序一致。所以不能 results.push(value),而要用任务启动时保存的 index 写回 results[index]。
fail-fast 模式适合任何一个失败都无法继续的场景,第一次 reject 后整体失败;all-settled 模式适合批量请求或局部容错,记录 fulfilled/rejected 状态并等待全部结束。
limit 要校验为正整数,空数组直接 resolve。同步异常要转成 rejected。fail-fast 后最好停止继续启动新任务,但已经运行中的任务无法真正取消,除非任务本身支持 AbortController。
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 创建后通常已经开始执行,调度器拿到时无法阻止它们并发。要传任务函数,让并发池决定何时调用。
push 得到的是完成顺序,不是输入顺序。批量请求通常需要结果与任务一一对应,所以要按启动时的 index 写回。
普通 Promise 不能被外部取消。可以停止启动新任务,但已经执行的任务只能等待结束;如果要取消,需要任务内部支持 AbortController 或自定义取消协议。
并发池限制同时运行的任务数量,关注队列调度;节流限制单位时间触发频率,关注事件触发频率。两者解决的问题不同。