由于Spectre和Meltdown的漏洞,所有主流浏览器在2018年1月就禁用了sharedArrayBuffer。
从2019年开始,有些浏览器开始逐步重新启用这一特性。
既不克隆,也不转移,sharedArrayBuffer作为ArrayBuffer能够在不同浏览器上下文间共享。
在把sharedArrayBuffer传给postMessage()时,浏览器只会传递原始缓冲区的引用。
结果是,两个不同的JavaScript上下文会分别维护对同一个内存块的引用。每个上下文都可以随意修改这个缓冲区,就跟修改常规ArrayBuffer一样。
多个上下文访问SharedArrayBuffer时,如果同时对缓冲区执行操作,就可能出现资源争用问题。
Atomics API通过强制同一时刻只能对缓冲区执行一个操作,可以让多个上下文安全地读写一个SharedArrayBuffer。
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| SharedArrayBuffer | SharedArrayBuffer和ArrayBuffer具有同样的API。 *** ** * ** *** 在多个执行上下文间共享内存意味着并发线程操作成为可能。 *** ** * ** *** 传统JavaScript操作对于并发内存访问导致的资源争用没有提供保护。 *** ** * ** *** Atomics API解决了这个问题,可以保证SharedArrayBuffer上的JavaScript操作是线程安全的。 |
| 原子操作基础 | 任何全局上下文中都有一个Atomics 对象,对象上暴露了用于执行线程安全操作的一套静态方法。 |
SharedArrayBuffer 和 Atomics API 详解
一、SharedArrayBuffer(共享内存)
1. 基本概念
-
共享内存:允许多个线程(Web Workers)共享同一块内存区域
-
主要用途:在多线程环境中高效地共享和操作数据
-
特点:在 Web Workers 之间传递时不复制数据,而是共享引用
2. 基本使用
javascript
javascript
// 主线程
const sharedBuffer = new SharedArrayBuffer(1024); // 创建1KB共享内存
const int32View = new Int32Array(sharedBuffer); // 创建视图
// 传递给 Worker
const worker = new Worker('worker.js');
worker.postMessage(sharedBuffer);
// Worker 中接收
// self.onmessage = function(e) {
// const sharedBuffer = e.data;
// const int32View = new Int32Array(sharedBuffer);
// }
3. 安全要求
- 必须设置 COOP/COEP 头部:
http
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
二、Atomics API
1. 为什么需要 Atomics?
-
解决多线程并发访问时的竞争条件问题
-
确保操作的原子性(atomic operations)
-
提供内存同步机制
2. 主要方法分类
原子操作(读写和运算):
javascript
javascript
const sharedBuffer = new SharedArrayBuffer(16);
const intArray = new Int32Array(sharedBuffer);
// 基础原子操作
Atomics.store(intArray, 0, 42); // 原子写入
let value = Atomics.load(intArray, 0); // 原子读取
// 原子运算
Atomics.add(intArray, 0, 5); // 原子加法(返回旧值)
Atomics.sub(intArray, 0, 3); // 原子减法
Atomics.and(intArray, 0, 0b1111); // 原子与运算
Atomics.or(intArray, 0, 0b1100); // 原子或运算
Atomics.xor(intArray, 0, 0b1010); // 原子异或运算
// 原子交换
let old = Atomics.exchange(intArray, 0, 100); // 交换为新值
let result = Atomics.compareExchange(
intArray,
0,
expectedValue,
newValue
); // 仅在当前值等于期望值时交换
同步和等待:
javascript
javascript
// 等待和通知机制(类似锁)
Atomics.wait(intArray, index, expectedValue, timeout);
Atomics.notify(intArray, index, count); // 唤醒等待的线程
// 使用示例
// Worker 1:
Atomics.store(intArray, 0, 0);
Atomics.wait(intArray, 0, 0); // 等待值变为非0
// Worker 2:
Atomics.store(intArray, 0, 1);
Atomics.notify(intArray, 0, 1); // 唤醒一个等待的Worker
3. 典型用例示例
生产者-消费者模式:
javascript
javascript
// 创建共享内存
const sharedBuffer = new SharedArrayBuffer(12);
const sharedArray = new Int32Array(sharedBuffer);
// 主线程(生产者)
const worker = new Worker('consumer.js');
worker.postMessage(sharedBuffer);
// 生产者逻辑
function produce(data) {
// 等待缓冲区有空位
while (Atomics.load(sharedArray, 0) !== 0) {
Atomics.wait(sharedArray, 0, 1);
}
// 写入数据
Atomics.store(sharedArray, 2, data);
// 设置标志通知消费者
Atomics.store(sharedArray, 0, 1);
Atomics.notify(sharedArray, 0, 1);
}
// Worker(消费者)
self.onmessage = function(e) {
const sharedArray = new Int32Array(e.data);
while (true) {
// 等待有数据可读
if (Atomics.load(sharedArray, 0) === 0) {
Atomics.wait(sharedArray, 0, 0);
continue;
}
// 读取数据
const data = Atomics.load(sharedArray, 2);
// 处理数据
console.log('Consumed:', data);
// 重置标志通知生产者
Atomics.store(sharedArray, 0, 0);
Atomics.notify(sharedArray, 0, 1);
}
};
互斥锁实现:
javascript
javascript
class Mutex {
constructor(sharedArray, index = 0) {
this.lock = new Int32Array(sharedArray);
this.index = index;
}
acquire() {
while (true) {
// 尝试获取锁(0表示未锁定)
if (Atomics.compareExchange(this.lock, this.index, 0, 1) === 0) {
return;
}
// 等待锁释放
Atomics.wait(this.lock, this.index, 1);
}
}
release() {
// 释放锁
Atomics.store(this.lock, this.index, 0);
Atomics.notify(this.lock, this.index, 1);
}
execute(callback) {
this.acquire();
try {
return callback();
} finally {
this.release();
}
}
}
三、最佳实践和注意事项
1. 性能考虑
-
尽量减少共享内存的访问
-
使用适当大小的内存块
-
避免频繁的跨线程通信
2. 错误处理
javascript
javascript
try {
// 检查浏览器支持
if (typeof SharedArrayBuffer !== 'undefined') {
const buffer = new SharedArrayBuffer(1024);
}
} catch (error) {
console.error('SharedArrayBuffer not supported:', error);
}
3. 调试技巧
-
使用断点和内存查看器
-
添加详细的日志记录
-
实现超时机制防止死锁
四、浏览器支持和安全限制
支持情况:
-
Chrome 68+(需要安全上下文)
-
Firefox 79+
-
Safari 15.4+
-
Edge 79+
安全限制:
-
必须使用 HTTPS
-
需要正确的 HTTP 头部
-
某些 API 可能被限制使用
总结
SharedArrayBuffer 和 Atomics API 为 JavaScript 带来了真正的多线程编程能力,但同时也增加了复杂性。使用时需要注意:
-
线程安全:始终使用原子操作访问共享内存
-
性能:避免不必要的共享访问
-
调试:多线程调试较为困难,需有良好设计
-
兼容性:检查浏览器支持并准备降级方案
这些 API 特别适用于:
-
高性能计算
-
实时数据处理
-
游戏和图形应用
-
大规模并行计算任务
补充 sharedArrayBuffer 示例
main.js
javascript
//main.js
//创建包含4个线程的线程池
const workers = [];
for (let i = 0; i < 4; i++) {
workers.push(new Worker("./js/worker_sharedArrayBuffer2.js"));
}
//在最后一个工作者线程完成后打印最终值
let count = 0;
for (const worker of workers) {
worker.onmessage = function () {
if (++count === workers.length) {
console.log(`final buffer value: ${view[0]}`);
}
};
}
//初始化SharedArrayBuffer
const buffer = new SharedArrayBuffer(4);
//创建缓冲区视图
const view = new Int32Array(buffer);
//设置初始值
view[0] = 1;
//发送缓冲区给所有线程
for (const worker of workers) {
worker.postMessage(buffer);
}
worker_sharedArrayBuffer1.js
在所有工作者线程读/写操作交织的过程中就会发生资源争用。
javascript
self.onmessage= ({data}) => {
const view = new Int32Array(data);
console.log(`buffer in worker: ${view[0]}`);
//为共享缓冲区赋值
//执行100万次加操作
for(let i=0;i<1E6;i++) {
view[0]+=1;
}
//发送空消息,通知赋值完成
self.postMessage(null);
};
worker_sharedArrayBuffer2.js
使用 Atomics.add() 可以得到正确的值。
javascript
//使用Atomics对象解决资源争用
self.onmessage= ({data}) => {
const view = new Int32Array(data);
console.log(`buffer in worker: ${view[0]}`);
//为共享缓冲区赋值
//执行100万次加操作
for(let i=0;i<1E6;i++) {
//使用Atomics对象解决资源争用
Atomics.add(view, 0, 1);
}
//发送空消息,通知赋值完成
self.postMessage(null);
}
Atomics 对象方法总结表
| 方法类别 | 方法名 | 语法 | 描述 | 返回值 |
|---|---|---|---|---|
| 原子操作 | store() |
Atomics.store(typedArray, index, value) |
原子方式将给定值存储在数组的指定位置 | 设置的值 |
load() |
Atomics.load(typedArray, index) |
原子方式从数组的指定位置读取值 | 读取的值 | |
exchange() |
Atomics.exchange(typedArray, index, value) |
原子方式将指定位置的值替换为新值 | 替换前的旧值 | |
compareExchange() |
Atomics.compareExchange(typedArray, index, expectedValue, replacementValue) |
仅在当前值等于期望值时原子替换为新值 | 替换前的旧值(无论是否替换) | |
| 原子运算 | add() |
Atomics.add(typedArray, index, value) |
原子加法:将指定值加到当前位置的值上 | 运算前的旧值 |
sub() |
Atomics.sub(typedArray, index, value) |
原子减法:从当前位置的值减去指定值 | 运算前的旧值 | |
and() |
Atomics.and(typedArray, index, value) |
原子按位与:与指定值进行按位与运算 | 运算前的旧值 | |
or() |
Atomics.or(typedArray, index, value) |
原子按位或:与指定值进行按位或运算 | 运算前的旧值 | |
xor() |
Atomics.xor(typedArray, index, value) |
原子按位异或:与指定值进行按位异或运算 | 运算前的旧值 | |
| 同步等待 | wait() |
Atomics.wait(typedArray, index, expectedValue, timeout) |
使线程等待,直到指定位置的值发生变化或超时 | "ok"(正常唤醒), "timed-out"(超时), "not-equal"(值不匹配) |
notify() |
Atomics.notify(typedArray, index, count) |
唤醒正在等待指定位置的线程 | 成功唤醒的线程数量 | |
| 实用方法 | isLockFree(size) |
Atomics.isLockFree(size) |
检查指定大小的操作是否在硬件层面是无锁的 | true(无锁)或false(需要锁) |
参数说明表
| 参数 | 类型 | 描述 |
|---|---|---|
typedArray |
Int8Array Uint8Array Int16Array Uint16Array Int32Array Uint32Array BigInt64Array BigUint64Array |
共享的TypedArray对象,基于SharedArrayBuffer创建 |
index |
number |
在typedArray中操作的索引位置 |
value |
number 或 BigInt |
要存储、添加或操作的值(类型需与TypedArray匹配) |
expectedValue |
number 或 BigInt |
期望的当前值(用于compareExchange和wait) |
replacementValue |
number 或 BigInt |
替换的新值 |
timeout |
number |
最大等待时间(毫秒),可选,默认无限等待 |
count |
number |
要唤醒的等待线程数量,可选,默认唤醒所有 |
size |
number |
字节大小(通常为1、2、4、8、16) |
使用场景速查表
| 场景 | 推荐方法 | 示例用途 |
|---|---|---|
| 基本读写 | store() / load() |
简单的线程安全读写操作 |
| 计数器 | add() / sub() |
多线程共享计数器 |
| 状态标志 | compareExchange() |
实现锁、信号量等同步原语 |
| 线程等待 | wait() / notify() |
生产者-消费者模式、条件等待 |
| 位操作 | and() / or() / xor() |
位标志、状态机控制 |
| 数据交换 | exchange() |
无锁队列、缓冲区交换 |
注意事项表
| 项目 | 说明 |
|---|---|
| 类型一致性 | TypedArray的类型必须与操作的值类型匹配 |
| 索引边界 | 索引必须在TypedArray的有效范围内 |
| 超时处理 | wait()的timeout参数控制最大等待时间 |
| 线程唤醒 | notify()可指定唤醒线程数量,避免"惊群效应" |
| 性能优化 | isLockFree()可帮助选择最优的数据大小 |
| 浏览器支持 | 部分浏览器可能对某些TypedArray类型支持有限 |
典型示例代码片段
javascript
javascript
// 创建共享内存
const sharedBuffer = new SharedArrayBuffer(1024);
const int32Array = new Int32Array(sharedBuffer);
// 原子写入和读取
Atomics.store(int32Array, 0, 42);
let value = Atomics.load(int32Array, 0); // 42
// 原子运算
let oldValue = Atomics.add(int32Array, 0, 10); // 返回42,位置0的值变为52
// 比较并交换
let result = Atomics.compareExchange(int32Array, 0, 52, 100); // 成功返回52
// 等待和通知
// 线程1:
Atomics.wait(int32Array, 0, 100); // 等待值改变
// 线程2:
Atomics.store(int32Array, 0, 200);
Atomics.notify(int32Array, 0, 1); // 唤醒一个等待线程