题干与适用场景
题目考察浏览器并发、共享内存和生命周期治理。Atomics.waitAsync() 在共享整数位置满足条件前返回 Promise,调用方可以继续处理 UI;它只能用于基于 SharedArrayBuffer 的 Int32Array 或 BigInt64Array 视图。回答应把等待协议、数据所有权、取消语义和降级路径连成完整方案。
面试官考察什么
- 能否区分主线程可用的非阻塞等待与 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)。示例协议如下:
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;应写入取消状态并通知等待者。等待方收到 ok、timed-out 或 not-equal 后都重新读取状态,再决定继续、重试或退出。关闭流程先停止生产,再把状态改为关闭并通知所有等待者,最后等待 Worker 确认退出,避免释放仍被访问的 SharedArrayBuffer。
4. 处理并发竞态和背压
多个生产者需要用 CAS 或分配器领取槽位,不能让它们同时写同一序号。队列满时根据业务选择丢弃、覆盖或背压,并记录队列深度和丢弃数。消费者不能把一次唤醒当成一条消息,应该循环读取所有已发布序号,直到队列为空或收到取消状态。
5. 管理 Worker 崩溃与页面生命周期
主线程维护 Worker 状态机和心跳;发生 error 或超时就停止新写入,标记任务失败并让剩余 Worker 退出。页面进入后台或组件卸载时发送取消通知,等待有限时间后终止 Worker。重启时创建新版本的控制区,避免复用含有旧序号和旧状态的内存。
6. 做能力检测和安全降级
启动时检查 crossOriginIsolated、SharedArrayBuffer、Atomics.waitAsync 和 Worker 能力。跨源隔离会影响第三方资源和弹窗,不能为了性能绕过安全策略。不满足条件时使用 Transferable ArrayBuffer 分块或普通 postMessage,并保留同样的取消、超时、进度和错误字段;监控各路径占比和尾延迟。
高质量示范回答
我会先做能力检测并确认页面满足安全上下文和跨源隔离要求,再建立版本化控制区。生产者写完数据后用原子存储发布状态并通知,消费者在状态仍为等待时调用 Atomics.waitAsync,无论 Promise 以何种结果结束,都重新读取状态和序号。取消、超时和关闭都是显式状态,关闭顺序是停止生产、通知等待者、确认 Worker 退出、最后释放共享内存。队列用所有权和序号避免重复消费,满载策略按业务选择背压或丢弃并记录指标。若共享内存或 API 不可用,就切换到 Transferable 分块消息,保持相同的生命周期和错误协议。
常见错误
- 认为
notify保证一条消息对应一次唤醒。 - 在主线程调用会阻塞的等待方式,造成页面卡顿。
- 只等待 Promise 结果,不重新读取共享状态和序号。
- 取消时直接释放缓冲区,留下仍在运行的 Worker。
- 忽略
SharedArrayBuffer的跨源隔离与第三方资源约束。 - 降级到
postMessage后丢失超时、取消或错误语义。
追问及应对
waitAsync 和 wait 如何选择?
waitAsync 返回 Promise,不阻塞调用线程,适合主线程或需要继续处理事件的代码;wait 可阻塞 Worker,但必须保证阻塞不会影响 UI 或其他关键任务。两者都要配合状态检查、超时和关闭协议。
为什么通知后还要重新读取状态?
通知只提示某个等待条件可能变化,可能出现合并通知、竞争唤醒或状态已被其他消费者改变。重新读取状态、序号和长度才能决定是否有可处理数据。
如何避免取消后又处理一批数据?
把取消写入共享状态,并在取槽、处理前和提交结果前各检查一次;结果提交也带任务版本。主线程撤销版本后,旧 Worker 的结果必须被丢弃。
何时应放弃共享内存?
当隔离头会破坏关键依赖、浏览器覆盖不足、调试成本超过收益或数据吞吐不高时,应使用 Transferable 分块。通过能力和指标选择路径,不把共享内存当作默认优化。