先记住这个答案
ArrayBuffer 是一段固定长度的原始字节,同一时刻只属于一个线程。通过 postMessage 发给 Worker 时,默认会复制一份;放进转移列表的话,内存直接交给 Worker,发送方的 ArrayBuffer 长度变成 0,不能再用。SharedArrayBuffer 则是主线程和多个 Worker 共用同一块内存,一方写入另一方马上能读到,省去了复制。但并发读写会出现竞争,比如两个线程同时对一个计数器加 1,结果可能只加了一次,所以要用 Atomics 提供的原子操作。大文件交给一个 Worker 处理用 ArrayBuffer 转移就够了,多个线程需要持续共享状态时才用 SharedArrayBuffer。
ArrayBuffer发给 Worker 只会复制或转移SharedArrayBuffer多个线程看到同一块内存- 共享不等于线程安全,
value++不是原子的 - 用
SharedArrayBuffer要开启跨源隔离
用 Atomics 修改共享计数器
下面在同一个线程里用两个视图指向同一块共享内存,模拟两个线程各自持有一个视图的情况。
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT) // 4 个字节
const firstView = new Int32Array(shared)
const secondView = new Int32Array(shared)
console.log(Atomics.add(firstView, 0, 2)) // 返回加之前的值
console.log(Atomics.add(secondView, 0, 3))
console.log(Atomics.load(firstView, 0)) // 两次加法都生效了查看输出与解释
0
2
5Atomics.add 返回的是修改前的值,所以依次输出 0 和 2。两个视图指向同一块内存,最后读出来是 5。真实场景里,firstView 在主线程,secondView 在 Worker 里,Worker 通过 postMessage 收到的 shared 就是同一块内存。
为什么共享内存需要原子操作
view[0]++ 看起来是一步,实际上是三步:读出当前值、加 1、写回去。两个线程同时执行时,可能都读到了 0,各自加 1 后都写回 1,结果少加了一次。这叫竞态条件。
Atomics能做什么Atomics.add、Atomics.sub、Atomics.compareExchange这些方法把读改写合成一个不会被打断的操作,别的线程看不到中间状态。Atomics.wait和Atomics.notify能让一个线程等待、另一个线程唤醒它,用来实现锁和信号。注意浏览器主线程不允许调用Atomics.wait阻塞,主线程里要等待的话,可以在支持的浏览器里用非阻塞的Atomics.waitAsync。 大多数前端场景其实用不上这些。Worker 处理完把结果发回来,用postMessage传消息就够了,状态不共享也就没有竞争。
浏览器里的启用条件
SharedArrayBuffer 能配合 Worker 做出高精度计时器,可能被用来发起 Spectre 这类侧信道攻击。所以浏览器要求页面处于跨源隔离状态才开放它:响应头要带 Cross-Origin-Opener-Policy: same-origin 和 Cross-Origin-Embedder-Policy: require-corp。
- 检查是否可用
- 页面里可以读
crossOriginIsolated,为true才能正常使用SharedArrayBuffer。开启require-corp后,页面加载的跨域图片、脚本都要带 CORS 或Cross-Origin-Resource-Policy响应头,否则会被拦截,接入第三方资源多的站点要先评估影响。
容易答错的地方
- 以为
Atomics只能用在SharedArrayBuffer上 Atomics.add、Atomics.load这些方法在普通ArrayBuffer上的整数类型数组也能调用,只是单线程里没什么意义。真正要求共享内存的是Atomics.wait,它只接受基于SharedArrayBuffer的Int32Array或BigInt64Array。另外Atomics只支持整数类型数组,Float64Array和Uint8ClampedArray都不行。- 以为用了
SharedArrayBuffer就是线程安全的 - 共享只是让多个线程看到同一块内存,普通读写之间依然会互相覆盖。需要并发修改的位置,都要用
Atomics的方法,复杂的状态还要自己设计加锁的规则。
面试官还会怎么问?
什么时候优先用 ArrayBuffer 转移?
数据很大、交给一个 Worker 处理、主线程之后不再需要原数据时,比如把一段视频帧交给 Worker 解码。写成 worker.postMessage(buffer, [buffer]),内存直接移交,不需要复制,也不用处理同步问题。
转移之后原来的 ArrayBuffer 会怎样?
它会变成「已分离」状态,byteLength 变成 0,基于它创建的类型化数组长度也变成 0,读出来都是 undefined,调用 slice() 这类方法会抛 TypeError。
Node.js 里也有 SharedArrayBuffer 吗?
有,配合 worker_threads 使用,不需要浏览器那样的跨源隔离设置。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。