SharedArrayBuffer 和 Atomics API 详解(附:Atomics 对象方法总结表)

由于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+


安全限制:

  1. 必须使用 HTTPS

  2. 需要正确的 HTTP 头部

  3. 某些 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 numberBigInt 要存储、添加或操作的值(类型需与TypedArray匹配)
expectedValue numberBigInt 期望的当前值(用于compareExchange和wait)
replacementValue numberBigInt 替换的新值
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); // 唤醒一个等待线程
相关推荐
Irene19915 天前
专用工作者线程两种终止方式对比(附:优雅关闭,关闭后行为分析)
工作者线程
Irene199112 天前
在 Vue 3 中使用 工作者线程
vue.js·工作者线程
Irene199113 天前
图示:浏览器、主线程、工作者线程之间的关系和通信方式(附:ArrayBuffer 详解)
浏览器·主线程·工作者线程