代表性面试主题

前端面试:如何用 Atomics.waitAsync 协调 Worker 并支持取消

前端困难
Offer.cc 编辑团队发布 更新

题干

你要让多个 Web Worker 协作处理实时数据,主线程不能被阻塞。请用 Atomics.waitAsync 设计一个等待与通知协议,并说明如何处理超时、取消、Worker 崩溃和不支持共享内存的浏览器。

题干与适用场景

题目考察浏览器并发、共享内存和生命周期治理。Atomics.waitAsync() 在共享整数位置满足条件前返回 Promise,调用方可以继续处理 UI;它只能用于基于 SharedArrayBufferInt32ArrayBigInt64Array 视图。回答应把等待协议、数据所有权、取消语义和降级路径连成完整方案。

面试官考察什么

  • 能否区分主线程可用的非阻塞等待与 Worker 中可能阻塞的 Atomics.wait()
  • 能否定义状态值、通知时机、超时和重复通知下仍成立的不变量。
  • 能否处理取消、页面隐藏、Worker 崩溃和共享缓冲区释放。
  • 能否说明跨源隔离、浏览器能力检测和 Transferable 降级的影响。

回答前需要澄清的问题

先确认数据是否允许丢弃、目标延迟、生产者和消费者数量、是否需要跨 Worker 共享,以及浏览器支持矩阵。还要确认页面能否启用安全上下文和 cross-origin isolation,第三方脚本、iframe 或登录弹窗是否受 COOP/COEP 影响。最后确定取消是用户主动停止、超时,还是页面生命周期结束。

30 秒回答框架

我会把共享区分成控制字和数据区,控制字使用 0=等待、1=就绪、2=取消、3=关闭 等版本化状态。消费者先原子读取状态,若仍为等待就调用 Atomics.waitAsync,生产者写入数据后用 Atomics.store 发布状态并 Atomics.notify。Promise 只表示等待结果,真正处理前仍需重新读取状态。超时和取消通过状态转换实现,所有 Worker 退出后再释放缓冲区;不支持共享内存时改用 Transferable 分块消息,保持相同的取消和错误语义。

分步骤深入解答

1. 设计共享状态和所有权

控制区至少包含协议版本、状态、序号、长度和关闭标记。生产者只写入自己拥有的槽位,完成写入后才发布就绪状态;消费者读取状态和序号后处理数据,不能依赖一次通知来保存消息。每次唤醒都要重新检查状态,因为通知可能合并、提前到达或对应上一批数据。

2. 正确使用 waitAsync 与 notify

Atomics.waitAsync(view, index, expected, timeout) 会先比较当前位置;值不等于 expected 时立即返回 not-equal,超时返回 timed-out,否则返回可等待的 Promise。生产者更新状态后调用 Atomics.notify(view, index, count)。示例协议如下:

js
const control = new Int32Array(new SharedArrayBuffer(16));
const WAITING = 0;
const READY = 1;
const CANCELLED = 2;

async function waitForData(timeout = 1000) {
  const result = Atomics.waitAsync(control, 0, WAITING, timeout);
  const outcome = result.async ? await result.value : result.value;
  const state = Atomics.load(control, 0);
  return { outcome, state };
}

function publishData() {
  Atomics.store(control, 0, READY);
  Atomics.notify(control, 0, 1);
}

3. 加入取消、超时和关闭协议

不要试图中断已经创建的 Promise;应写入取消状态并通知等待者。等待方收到 oktimed-outnot-equal 后都重新读取状态,再决定继续、重试或退出。关闭流程先停止生产,再把状态改为关闭并通知所有等待者,最后等待 Worker 确认退出,避免释放仍被访问的 SharedArrayBuffer

4. 处理并发竞态和背压

多个生产者需要用 CAS 或分配器领取槽位,不能让它们同时写同一序号。队列满时根据业务选择丢弃、覆盖或背压,并记录队列深度和丢弃数。消费者不能把一次唤醒当成一条消息,应该循环读取所有已发布序号,直到队列为空或收到取消状态。

5. 管理 Worker 崩溃与页面生命周期

主线程维护 Worker 状态机和心跳;发生 error 或超时就停止新写入,标记任务失败并让剩余 Worker 退出。页面进入后台或组件卸载时发送取消通知,等待有限时间后终止 Worker。重启时创建新版本的控制区,避免复用含有旧序号和旧状态的内存。

6. 做能力检测和安全降级

启动时检查 crossOriginIsolatedSharedArrayBufferAtomics.waitAsync 和 Worker 能力。跨源隔离会影响第三方资源和弹窗,不能为了性能绕过安全策略。不满足条件时使用 Transferable ArrayBuffer 分块或普通 postMessage,并保留同样的取消、超时、进度和错误字段;监控各路径占比和尾延迟。

高质量示范回答

我会先做能力检测并确认页面满足安全上下文和跨源隔离要求,再建立版本化控制区。生产者写完数据后用原子存储发布状态并通知,消费者在状态仍为等待时调用 Atomics.waitAsync,无论 Promise 以何种结果结束,都重新读取状态和序号。取消、超时和关闭都是显式状态,关闭顺序是停止生产、通知等待者、确认 Worker 退出、最后释放共享内存。队列用所有权和序号避免重复消费,满载策略按业务选择背压或丢弃并记录指标。若共享内存或 API 不可用,就切换到 Transferable 分块消息,保持相同的生命周期和错误协议。

常见错误

  • 认为 notify 保证一条消息对应一次唤醒。
  • 在主线程调用会阻塞的等待方式,造成页面卡顿。
  • 只等待 Promise 结果,不重新读取共享状态和序号。
  • 取消时直接释放缓冲区,留下仍在运行的 Worker。
  • 忽略 SharedArrayBuffer 的跨源隔离与第三方资源约束。
  • 降级到 postMessage 后丢失超时、取消或错误语义。

追问及应对

waitAsyncwait 如何选择?

waitAsync 返回 Promise,不阻塞调用线程,适合主线程或需要继续处理事件的代码;wait 可阻塞 Worker,但必须保证阻塞不会影响 UI 或其他关键任务。两者都要配合状态检查、超时和关闭协议。

为什么通知后还要重新读取状态?

通知只提示某个等待条件可能变化,可能出现合并通知、竞争唤醒或状态已被其他消费者改变。重新读取状态、序号和长度才能决定是否有可处理数据。

如何避免取消后又处理一批数据?

把取消写入共享状态,并在取槽、处理前和提交结果前各检查一次;结果提交也带任务版本。主线程撤销版本后,旧 Worker 的结果必须被丢弃。

何时应放弃共享内存?

当隔离头会破坏关键依赖、浏览器覆盖不足、调试成本超过收益或数据吞吐不高时,应使用 Transferable 分块。通过能力和指标选择路径,不把共享内存当作默认优化。

公开来源

同类题目