SharedArrayBuffer 是支持多线程共享的原始内存,需通过类型化视图访问,且依赖跨域隔离策略;主线程须用 transfer 机制移交所有权给 Worker;读写必须用 Atomics 原子操作,不可直接赋值;结构体共享需手动布局内存并注意视图类型限制。

SharedArrayBuffer 是什么,为什么不能直接 new 了就用
SharedArrayBuffer 本身只是分配一块可被多个 Worker 同时访问的原始内存,但它不提供任何同步机制,也不带类型视图。直接 new SharedArrayBuffer(1024) 得到的是一块“裸内存”,读写前必须通过 Int32Array、Float64Array 等视图绑定,否则会报 TypeError: invalid ArrayBuffer for this operation。
更重要的是:Chrome/Firefox 要求页面启用跨域隔离(crossorigin + COOP/COEP 头),否则 SharedArrayBuffer 构造会静默失败或抛出 ReferenceError: SharedArrayBuffer is not defined。不是代码写错了,是环境没配对。
- 检查是否设置了
Cross-Origin-Embedder-Policy: require-corp和Cross-Origin-Opener-Policy: same-origin - 主页面 script 标签需加
crossorigin属性,且服务端返回的 JS 文件也要同源或带Access-Control-Allow-Origin - 本地
file://协议下永远不可用,必须走http://或https://
怎么让主线程和 Worker 安全地读写同一块内存
不能靠轮询,也不能靠普通 postMessage 传数据——那又回到拷贝了。核心是:主线程创建 SharedArrayBuffer,通过 postMessage 的 transfer 机制把所有权移交 Worker,双方再各自构建相同偏移和类型的视图。
关键点在于「移交」而非「复制」:postMessage(buffer, [buffer]) 中第二个参数是 transfer list,执行后主线程的 buffer 变成 undefined,Worker 收到的是同一块内存的引用。
- 主线程示例:
const sab = new SharedArrayBuffer(1024); const view = new Int32Array(sab); view[0] = 1; <p>const worker = new Worker('worker.js'); worker.postMessage({sab}, [sab]); // ← 必须在这里 transfer - Worker 内:
self.onmessage = ({data}) => { const {sab} = data; const view = new Int32Array(sab); // 视图类型和长度必须与主线程一致 console.log(view[0]); // → 1 }; - 如果忘了
[sab],Worker 收到的是null;如果主线程后续还试图读view[0],会得到NaN或静默失败(因为 buffer 已被转移)
没有锁,怎么避免竞态?Atomics 是唯一靠谱的选择
SharedArrayBuffer 上的普通读写(如 view[0]++)不是原子操作,在多线程下必然出错。必须用 Atomics API,它提供内存屏障、等待唤醒、条件更新等底层原语。
常见误用是把 Atomics.add 当成“加法函数”随便调:Atomics.add(view, 0, 1) 没问题,但 view[0] = Atomics.add(view, 0, 1) 就多余且危险——Atomics.add 返回的是旧值,赋值反而覆盖了原子结果。
- 增减计数器:
Atomics.add(view, index, delta),返回修改前的值 - 条件写入(CAS):
Atomics.compareExchange(view, index, oldVal, newVal),仅当当前值等于oldVal才写入newVal - 等待与唤醒:
Atomics.wait(view, index, value, timeoutMs)阻塞直到该位置值变化;Atomics.notify(view, index, count)唤醒最多count个等待者 - 所有
Atomics方法第一个参数必须是TypedArray(如Int32Array),不能传SharedArrayBuffer本身
实际共享结构体数据时,怎么规划内存布局
共享一个数字或布尔值太简单,真实场景常要共享对象(比如帧数据、音频采样、粒子状态)。这时得手动做内存布局:把字段按类型和对齐要求排布,用固定 offset 访问。
例如共享一个含 position.x/y/z(float32)和 life(int32)的粒子结构体,每个实例占 4 * 3 + 4 = 16 字节。第 i 个粒子的 x 就在 offset = i * 16 处,用 floatView[i * 4] 读,intView[i * 4 + 3] 读 life —— 但要注意:不同视图指向同一块内存,不能混用索引。
- 推荐封装一个类,内部持有一个
SharedArrayBuffer和多个 typed view(Float32Array、Int32Array),用方法隐藏 offset 计算 - 避免频繁新建视图:
new Float32Array(sab, byteOffset, length)开销小,但反复调用仍不如复用已有视图 - 结构体字段顺序影响 cache line 利用率,高频读写的字段尽量靠近(如把
position和velocity放一起),冷字段(如调试标记)往后放
最易被忽略的是:Atomics.wait 只支持 Int32Array 和 BigInt64Array,哪怕你共享的是 float 数据,也得单独划出 int 字段做信号量或状态位——别试图对 Float32Array 调用 wait,会直接抛错。

















