题干与适用场景
这道编程题考察并发调度,而不是把所有 Promise 放进 Promise.all。候选人需要在吞吐、顺序、错误传播、取消和空输入之间定义清晰契约,并证明不会超出并发上限或重复消费项目。
面试官考察什么
- 能否用固定 worker 或等价调度器保持最多 N 个活动任务。
- 能否按输入索引写回结果,不让完成顺序改变输出顺序。
- 能否明确 fail-fast 与收集错误的语义,并在拒绝后处理仍在运行的任务。
- 能否正确处理 N 非法、空输入、同步抛错、取消和 mapper 返回非 Promise 值。
回答前需要澄清的问题
先确认是否允许任务在取消后继续运行、fail-fast 时是否需要等待清理、是否要返回部分结果,以及 mapper 是否接受 AbortSignal。还要确认是否限制重试、是否允许动态调整并发、错误对象是否必须保留原始引用。
30 秒回答框架
我会创建不超过 N 个 worker,共享下一个索引。每个 worker 取一个索引,使用 Promise.resolve 包住同步返回和同步抛错,再按索引写入结果。fail-fast 只传播第一项错误并停止派发新任务;collect 模式记录每项状态。AbortSignal 触发后停止取新索引,并把信号传给 mapper。时间复杂度是 O(items),额外空间是结果与状态 O(items),峰值活动任务 O(N)。
分步骤深入解答
1. 先定义结果与错误契约
结果必须与输入同序。collect 模式返回 fulfilled/rejected 状态和原始错误,不能用 undefined 混淆成功值。fail-fast 返回第一个观察到的错误,但要说明已经开始的任务不会被 JavaScript 自动杀死;若 mapper 支持信号,可以主动请求取消。
2. 固定 worker 和索引分配
把下一个索引放在共享计数器中,每个 worker 循环取值,直到越界、取消或 fail-fast 标记出现。任务启动前递增索引,确保一个项目只被一个 worker 领取。worker 数量取 min(N, items.length),避免空闲 worker 和无意义的 Promise。
3. 处理同步抛错与完成顺序
调用 mapper 时要捕获同步抛错,再用 Promise.resolve 统一异步完成。成功或失败都按原索引写入状态;不能在完成回调里直接 push 到数组。一个 worker 完成后立刻领取下一个索引,以维持吞吐但不增加活动数量。
4. 传播取消并停止派发
如果 signal 已取消,先拒绝或返回约定的取消错误,不再启动新任务。运行中任务收到同一个 signal;调度器等待 worker 退出后再 settle,避免调用方看到主 Promise 已结束而后台仍无限派发。取消和业务错误要有可区分的错误原因。
5. 验证边界与复杂度
测试空数组、N 为 0、N 大于输入、mapper 同步返回、同步抛错、不同延迟、第一项失败、多个失败和中途取消。用计数器断言活动任务从未超过 N,用日志或测试钩子确认每个索引只调用一次。总 mapper 调用 O(items),结果空间 O(items),调度器额外并发 O(N)。
高质量示范回答
我会校验 N 为正整数,创建 min(N, items.length) 个 worker,共享下一个索引。worker 取到索引后调用 mapper,并用 Promise.resolve 处理同步值和同步抛错;结果按索引写回,所以完成顺序不会改变输出。fail-fast 设置停止派发标志并传播首个错误,collect 模式保留每项状态。signal 取消后不再领取新索引,并把 signal 传给运行中 mapper,等待 worker 退出。测试延迟乱序、同步错误、空输入、N 边界和取消,断言并发峰值不超过 N。
常见错误
- 直接
Promise.all(items.map(mapper)),一次性启动全部任务。 - 用
push收集结果,导致输出按完成顺序而不是输入顺序。 - 只捕获 Promise rejection,漏掉 mapper 的同步抛错。
- fail-fast 后继续派发新任务,或误以为 reject 会自动取消已运行任务。
- 把取消当成普通业务错误,导致调用方无法区分重试与用户主动停止。
- 没有验证 N、空输入和 mapper 返回普通值的边界。
追问及应对
为什么不用递归启动下一项?
递归可以表达串行链,但必须额外维护并发计数、错误和取消;固定 worker 更容易证明活动任务上限,也避免深递归和重复派发。
fail-fast 时已经开始的任务怎么办?
JavaScript Promise 没有通用强制终止。停止派发新任务,并在 mapper 支持 AbortSignal 时调用 abort;否则等待它们自然结束,同时避免再写入已对外结束的结果。
collect 模式应该怎样表达错误?
用每项状态对象区分 fulfilled 和 rejected,并保留原始 value 或 reason。不要用特殊占位值,因为成功值本身可能是 undefined 或 null。
如何动态调整并发?
把最大 worker 数和当前活动数分开管理,在安全边界增加或停止领取索引。动态调整会增加证明和测试成本,除非需求明确,否则先采用固定上限。