JavaScript Atomics.waitAsync非阻塞等待SharedArrayBuffer
JavaScript 主线程是单线程的,这意味着如果在主线程中执行耗时操作会阻塞页面渲染,导致用户界面卡顿。在使用 SharedArrayBuffer 进行多线程(Web Workers)共享内存操作时,传统的 Atomics.wait() 会直接挂起当前线程,如果在主线程调用,页面会彻底冻结。Atomics.waitAsync() 解决了这个问题,它允许线程等待共享内存的变化而不阻塞执行流,非常适合用于高性能的并发任务调度。
准备工作:配置服务器安全策略
由于安全原因,现代浏览器默认禁用 SharedArrayBuffer,除非页面处于“跨域隔离”环境。你需要设置特定的 HTTP 响应头才能使用相关功能。
-
打开你的 Web 服务器配置文件(如 Nginx 的
.conf文件或 Node.js 的 Express 中间件)。 -
添加以下两个响应头到配置中:
响应头名称 值 Cross-Origin-Embedder-Policyrequire-corpCross-Origin-Opener-Policysame-origin -
重启 Web 服务器使配置生效。
-
确认页面加载时无报错,且
typeof SharedArrayBuffer !== 'undefined'。
步骤一:创建主线程逻辑
主线程负责创建共享内存,并将其发送给 Worker,随后在适当的时候修改内存数据并通知 Worker。
-
创建一个名为
main.js的文件。 -
初始化一块
SharedArrayBuffer,大小为 4 字节(足以存储一个 32 位整数)。 -
创建一个
Int32Array视图来操作这块内存。 -
实例化 Web Worker。
-
编写发送内存给 Worker 的代码,并在 2 秒后修改内存值并通知。
// main.js // 1. 创建共享内存缓冲区(4字节,用于存一个整数) const sharedBuffer = new SharedArrayBuffer(4); const sharedArray = new Int32Array(sharedBuffer); // 初始值为 0,表示“未就绪” sharedArray[0] = 0; console.log('[Main] 内存已初始化,当前值:', sharedArray[0]); // 2. 创建 Worker const worker = new Worker('worker.js'); // 3. 将共享内存的所有权转移给 Worker(实际上是复制引用) worker.postMessage({ sharedBuffer }, [sharedBuffer]); // 4. 模拟异步操作,2秒后唤醒 Worker setTimeout(() => { console.log('[Main] 准备修改内存值并唤醒 Worker...'); // 更新共享内存的值 Atomics.store(sharedArray, 0, 1); // 通知正在等待的 Worker // 参数:数组, 索引, 等待唤醒的线程数量 Atomics.notify(sharedArray, 0, 1); console.log('[Main] 完成通知,当前值:', sharedArray[0]); }, 2000);
步骤二:编写 Worker 线程逻辑
Worker 线程接收共享内存,并使用 Atomics.waitAsync 等待主线程的信号。与 wait 不同,这里等待是非阻塞的,Worker 可以在等待期间处理其他任务。
-
创建一个名为
worker.js的文件。 -
监听主线程发送的
message事件以获取共享内存。 -
调用
Atomics.waitAsync进行非阻塞等待。 -
处理返回的 Promise 对象,一旦主线程发送通知,Promise 将 resolve。
// worker.js self.onmessage = function(e) { const sharedArray = new Int32Array(e.data.sharedBuffer); console.log('[Worker] 开始等待共享内存变化...'); // 使用 waitAsync 进行非阻塞等待 // 参数:数组, 索引, 期望值(如果不等于此值则立即返回) const result = Atomics.waitAsync(sharedArray, 0, 0); console.log('[Worker] waitAsync 已调用,不阻塞,我可以做别的事情。'); // 检查等待结果 if (result.async) { // 如果是异步等待,result.value 是一个 Promise result.value.then((value) => { console.log('[Worker] 收到通知!Promise resolved.'); console.log('[Worker] Atomics.waitAsync 返回的值:', value); console.log('[Worker] 当前内存值:', Atomics.load(sharedArray, 0)); }).catch(err => { console.error('[Worker] 出错了:', err); }); } else { // 同步返回(极少数情况,例如主线程不匹配或已经在主线程误用) console.log('[Worker] 同步返回:', result.value); } };
步骤三:运行并验证流程
通过 HTML 页面加载主线程脚本,观察控制台输出,验证非阻塞特性。
-
创建一个
index.html文件。 -
引入
main.js。<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Atomics.waitAsync Demo</title> </head> <body> <h1>打开控制台查看日志</h1> <script src="main.js"></script> </body> </html> -
启动本地服务器,访问
index.html。 -
打开浏览器开发者工具的 Console(控制台)面板。
-
观察日志输出顺序。
预期输出如下:
[Main] 内存已初始化,当前值: 0
[Worker] 开始等待共享内存变化...
[Worker] waitAsync 已调用,不阻塞,我可以做别的事情。
[Main] 准备修改内存值并唤醒 Worker...
[Main] 完成通知,当前值: 1
[Worker] 收到通知!Promise resolved.
[Worker] Atomics.waitAsync 返回的值: "ok"
[Worker] 当前内存值: 1
核心机制解析
Atomics.waitAsync 的核心在于它将“等待”行为封装成了一个 Promise,从而释放了事件循环。为了更直观地理解主线程与 Worker 之间的数据流向与控制权转移,请参考以下流程图:
线程继续执行,不阻塞 Worker-->>Main: (此时 Worker 闲置或处理其他任务) Note over Main, Mem: 通知阶段 Main->>Mem: 写入新值 1 Main->>Mem: 调用 notify() Note over Worker, Mem: 唤醒阶段 Mem-->>Worker: Promise Resolve ("ok") Worker->>Mem: 读取值 (1) Worker->>Worker: 执行 .then() 回调
关键注意事项与最佳实践
在使用 Atomics.waitAsync 时,务必遵守以下规则以确保程序正确且高效。
| 注意事项 | 说明 | 解决方案 |
|---|---|---|
| COOP/COEP 头部 | 缺少安全头部会导致 SharedArrayBuffer 未定义。 |
配置服务器响应头 Cross-Origin-Opener-Policy: same-origin 和 Cross-Origin-Embedder-Policy: require-corp。 |
| 期望值匹配 | waitAsync 的第三个参数是“期望值”。如果内存当前值不等于期望值,等待立即结束。 |
确保 store 操作改变值之前,waitAsync 已经处于等待状态且期望值一致。 |
| 索引越界 | 提供的索引超出了 TypedArray 的长度。 |
确保索引在 0 到 array.length - 1 之间。 |
| 非主线程限制 | 虽然 waitAsync 是非阻塞的,但在某些浏览器环境中,主线程上调用仍然受限于严格的策略。 |
建议在 Web Worker 中使用 waitAsync,主线程仅负责 notify。 |
- 使用
result.async属性来判断等待是否为异步模式。 - 避免 在
Promise回调中执行极其耗时的同步计算,否则仍会拖慢 Worker 响应后续消息。 - 利用
Atomics.notify的第三个参数(计数)来精确控制唤醒多少个等待中的 Worker,实现“只唤醒一个”或“广播唤醒所有”的策略。

暂无评论,快来抢沙发吧!