引言
如果你曾在浏览器中使用 Web Worker 或 Node.js 的 Worker Thread 编写多线程代码,那么你一定遇到过这两个概念:
ArrayBufferSharedArrayBuffer
它们的名字看起来只差一个 “Shared”,实则背后承载的是线程安全模型、内存隔离策略,甚至涉及到了浏览器的安全沙箱机制。
本篇文章将带你深入探索:
ArrayBuffer与SharedArrayBuffer的设计目的- 两者在内存隔离上的根本差异
- 如何在 Worker 中实现跨线程通信而不破坏数据一致性
- 浏览器为何曾禁用 SharedArrayBuffer,又如何通过 COOP/COEP 回归
- 实际案例:如何使用 SharedArrayBuffer 实现一个线程安全的计数器
一、背景知识:JS世界本来是单线程的
JavaScript 的运行模型是单线程的,意味着同一时间只能执行一段代码。为了避免 UI 卡顿,HTML5 引入了 Web Worker(浏览器)和 Worker Thread(Node.js),用来并行执行任务。
问题是,多线程就需要解决一个问题:数据怎么传?
- 主线程不能直接操作 Worker 里的变量。
- Worker 也不能“偷看”主线程的上下文。
- 所以只能靠:postMessage()。
但 postMessage() 是通过 结构化克隆算法 来传输数据的:
- 每传一次,就复制一次;
- 对大数据(二进制图像、音频、文件)极其低效;
- 无法实现真正的“共享”。
二、ArrayBuffer:传得了,看不到
ArrayBuffer 是 JavaScript 操作二进制数据的底层数据结构。它提供了一段连续的、固定长度的原始内存,用于存储数据。
const buffer = new ArrayBuffer(8); // 8字节
const view = new Uint8Array(buffer);
view[0] = 42;
特点
- 可用于
TypedArray(如Uint8Array,Float32Array)创建视图; - 可通过
postMessage()传入 Worker,但默认是复制; - 也可以作为“可转移对象”发送,发送后源线程失去访问权。
worker.postMessage(buffer, [buffer]); // Transfer
这个 “可转移对象机制” 是一种内存拥有权的转移,不是共享。发送方再想访问?对不起,byteLength = 0。
所以 ArrayBuffer 是一种“可搬家,但不共居”的模型。
三、SharedArrayBuffer:共享内存的特洛伊木马
SharedArrayBuffer 的出现正是为了解决上述问题:在多个线程间共享一块内存,同时保持数据一致性。
const shared = new SharedArrayBuffer(8);
const view = new Int32Array(shared);
view[0] = 123;
你可以把它传给多个 Worker,无需复制,大家直接指向同一块物理内存。
worker.postMessage(shared); // 不需要 transfer 列表
关键差异
| 特性 | ArrayBuffer | SharedArrayBuffer |
|---|---|---|
| 是否共享 | 否(复制/转移) | 是(直接引用) |
| 是否可转移 | 是 | 否(共享无须转移) |
| 可否在多个线程访问 | 否 | 是(并发访问) |
| 是否线程安全 | 否 | 否(需配合 Atomics) |
| 是否可直接修改 | 是 | 是 |
注意:SharedArrayBuffer 本身并不保证线程安全,你仍需使用 Atomics 提供的原子操作。
四、内存隔离机制详解
ArrayBuffer 的隔离策略
当你通过 postMessage() 传递 ArrayBuffer:
- 若不指定 transfer,则克隆;
- 若指定 transfer 列表,则把所有权转移,发送方“断电”;
- 两个线程的视图不会再指向同一块内存空间。
这是浏览器为 JS 实现的“堆隔离”策略 —— 避免线程交叉污染。
SharedArrayBuffer 的共享实现
SharedArrayBuffer 本质上由浏览器的底层内存模型支持:
- 使用共享内存区域(通常是平台提供的共享页内存)
- 多个线程可以访问相同地址空间
- 浏览器对该内存区域做了内存页锁定(Page Locking)处理,避免被 GC 误删
更关键的是,它跳过了结构化克隆的隔离机制,完全依赖开发者自己控制一致性。
五、为什么浏览器曾禁用 SharedArrayBuffer?
一个关键词:Spectre
Spectre 是一种处理器层面的旁路攻击漏洞,攻击者可以利用预测执行 + 缓存时序分析,越权读取别的线程的内存数据。
SharedArrayBuffer 刚好是“极佳入口”:
- 它共享物理内存;
- 可被 Worker 高速读写;
- 可被利用发起精确的时序测量攻击。
于是浏览器在 2018 年紧急禁用了它,直到提供了对应的安全缓解机制:
安全防护机制:COOP + COEP
COOP(Cross-Origin-Opener-Policy)COEP(Cross-Origin-Embedder-Policy)
配置如下 HTTP Header 才能启用 SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
你可以在浏览器控制台看到报错提示,不配这个头,SharedArrayBuffer 根本不能用。
六、原子性保障:Atomics 是你的好兄弟
SharedArrayBuffer 让线程能访问同一内存,但会有竞争条件(Race Condition)。
举例:
// 两个线程同时执行
view[0] += 1;
可能在底层变成:
- 线程 A 读了
view[0] = 1 - 线程 B 也读了
view[0] = 1 - A 写入
2 - B 也写入
2 - 最终结果是
2,但我们期望是3
解决办法是使用 Atomics:
Atomics.add(view, 0, 1); // 原子加 1
它保证了内存操作的原子性,底层依赖 CPU 的原子指令(如 x86 的 LOCK 前缀)。
你还能用:
Atomics.wait()+Atomics.notify()实现同步阻塞Atomics.compareExchange()实现锁竞争Atomics.store/load实现可见性保障
这是 JavaScript 走向并发编程的重要一步。
七、实际案例:线程安全的计数器
主线程:
const shared = new SharedArrayBuffer(4); // 4 字节 = 1个 Int32
const counter = new Int32Array(shared);
const worker = new Worker('./worker.js');
worker.postMessage(shared);
worker.js:
onmessage = function(e) {
const counter = new Int32Array(e.data);
for (let i = 0; i < 10000; i++) {
Atomics.add(counter, 0, 1);
}
postMessage('done');
}
这个方式可以安全地在多个线程对同一个变量进行累加操作,结果是稳定的、不会丢失。
八、未来展望:结构共享的新时代?
SharedArrayBuffer 与 Atomics 已经是 JS 并发编程的重要组成部分。未来的方向包括:
- WebAssembly 将进一步依赖
SharedArrayBuffer进行跨模块内存共享; - JS 多线程库(如
Comlink、Threads.js)会对其做封装; - React、Vue 等框架也可能会引入基于 SAB 的数据流模型优化。
但与此同时,安全机制始终是底线,未来浏览器将持续强化隔离能力和 CSP 限制。
总结
ArrayBuffer是单线程内存数据容器,可传可转,但不共享;SharedArrayBuffer是多线程共享内存工具,但需开发者确保一致性;- 内存隔离机制是浏览器基于结构化克隆 + 所有权控制实现的;
- 使用 SAB 时必须启用 COOP/COEP,防止 Spectre 攻击;
- 实现线程安全操作,需要
Atomics提供原子性保障。