JavaScript 原子操作

深入解析 JavaScript 原子操作机制:SharedArrayBuffer 与 Atomics API 的使用、原子操作的语义、以及在 Web Workers 中实现高效并发的基础设施。

为什么需要原子操作

JavaScript 传统上是单线程执行的,但现代 Web 平台通过 Web Workers 提供了多线程能力。然而,Workers 之间是隔离的,它们通过消息传递进行通信,无法直接共享内存。这在需要高性能并行处理的场景(如数据分析、物理模拟、加密计算)成了瓶颈。

SharedArrayBufferAtomics API 的出现改变了这一状况。它们允许 JavaScript 在多个线程之间共享内存,提供了真正的并行计算能力。


一、SharedArrayBuffer 基础

1.1 什么是 SharedArrayBuffer

SharedArrayBuffer 是一个表示二进制数据的容器,与 ArrayBuffer 类似,但可以在多个 Agent(主线程和 Workers)之间共享:

// 创建共享缓冲区,8 字节长度
const sharedBuffer = new SharedArrayBuffer(8);

// 在不同线程中访问同一个缓冲区
// Worker 1:
const view = new Int32Array(sharedBuffer);
view[0] = 42;

// Worker 2 (同一 sharedBuffer):
const view = new Int32Array(sharedBuffer);
console.log(view[0]); // 42 — 跨线程读取

1.2 安全问题:Spectre 和 Meltdown

SharedArrayBuffer 最初在 2017 年被引入浏览器,但由于 SpectreMeltdown 漏洞,2018 年初被完全禁用。这两个漏洞允许攻击者利用 CPU 的推测执行机制,从受害进程中读取任意内存。

浏览器重新启用 SharedArrayBuffer 的条件是实现了 Cross-Origin Isolation(跨域隔离),通过设置以下 HTTP 响应头:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

这些头部确保页面不与跨域文档共享浏览上下文,从而防止 Spectre 类攻击。

1.3 检测跨域隔离

if (typeof SharedArrayBuffer !== 'undefined') {
  // 检查是否在跨域隔离环境中
  if (self.crossOriginIsolated) {
    console.log('SharedArrayBuffer is available');
  } else {
    console.log('SharedArrayBuffer is NOT available — not cross-origin isolated');
  }
}

二、Atomics API

2.1 Atomics 的设计目标

Atomics 对象提供了一组静态方法,用于在共享内存上执行原子操作。原子操作保证在多线程环境中,操作是"不可分割"的——不会被其他线程的操作插断。

核心操作类型

  • 加载(Load):原子读取值
  • 存储(Store):原子写入值
  • 修改(Modify):读-改-写操作(add、sub、and、or、xor)
  • 交换(Exchange):原子替换值
  • 比较交换(Compare Exchange):条件原子写入

2.2 基础原子操作

const sharedBuffer = new SharedArrayBuffer(8);
const int32View = new Int32Array(sharedBuffer);

// 原子写入
Atomics.store(int32View, 0, 42);

// 原子读取
const value = Atomics.load(int32View, 0);
console.log(value); // 42

// 原子加法:返回旧值
const oldValue = Atomics.add(int32View, 0, 10);
console.log(Atomics.load(int32View, 0)); // 52

// 原子子减:返回旧值
const oldValue2 = Atomics.sub(int32View, 0, 5);
console.log(Atomics.load(int32View, 0)); // 47

2.3 原子交换与比较交换

// 原子交换:用新值替换指定位置的值,返回旧值
const oldValue = Atomics.exchange(int32View, 0, 100);
console.log(oldValue); // 47 (旧值)
console.log(Atomics.load(int32View, 0)); // 100 (新值)

// 原子比较交换(CAS):如果期望值匹配才写入
// compareExchange(buffer, index, expected, replacement)
// 返回:旧值(如果匹配则 replacement,否则返回原值)
const expected = 100;
const result = Atomics.compareExchange(int32View, 0, expected, 200);
console.log(result); // 100 — 匹配,写入成功

// 再次尝试,expected 已经是 200,不匹配
const result2 = Atomics.compareExchange(int32View, 0, 100, 300);
console.log(result2); // 200 — 不匹配,返回当前值,缓冲区不变

CAS 是实现无锁数据结构的核心操作

2.4 原子位操作

// 原子 AND
Atomics.store(int32View, 0, 0b1111); // 15
Atomics.and(int32View, 0, 0b1100);   // 0b1111 & 0b1100 = 0b1100 = 12

// 原子 OR
Atomics.store(int32View, 0, 0b0101); // 5
Atomics.or(int32View, 0, 0b0011);    // 0b0101 | 0b0011 = 0b0111 = 7

// 原子 XOR
Atomics.store(int32View, 0, 0b1010); // 10
Atomics.xor(int32View, 0, 0b0110);   // 0b1010 ^ 0b0110 = 0b1100 = 12

三、原子等待与通知

3.1 Atomics.wait 与 Atomics.notify

这两个操作提供了一种线程间同步机制

  • Atomics.wait(arr, index, value, timeout):在指定位置等待值变化
  • Atomics.notify(arr, index, count):通知等待的线程
// 主线程:等待 Worker 处理完成
const sharedBuffer = new SharedArrayBuffer(8);
const int32View = new Int32Array(sharedBuffer);

// 设置初始值
Atomics.store(int32View, 0, 0);

console.log('Waiting for worker to set value...');
// 等待 int32View[0] 变为非零值,最多等待 1000ms
const result = Atomics.wait(int32View, 0, 0, 1000);
console.log('Wait result:', result); // 'ok', 'not-equal', or 'timed-out'

// Worker 线程:处理完成后通知主线程
Atomics.store(int32View, 0, 1); // 写入完成标志
Atomics.notify(int32View, 0, 1); // 通知 1 个等待的线程

3.2 使用场景

原子等待/通知适用于:

  1. 生产者-消费者模式:Worker 生产数据,主线程消费
  2. 屏障同步:等待所有 Worker 完成后再继续
  3. 锁实现:基于 CAS 和 wait/notify 实现自定义锁

3.3 注意事项

Atomics.wait 只能在 main thread 调用,在 Worker 中调用会抛出错误。Worker 应该使用 Atomics.waitAsync(ES2023):

// Worker 中使用 waitAsync
const result = Atomics.waitAsync(int32View, 0, 0, 1000);
result.value.then(r => console.log('Wait result:', r));

四、实用模式

4.1 简单的计数器

class AtomicCounter {
  constructor(initialValue = 0) {
    this.buffer = new SharedArrayBuffer(4);
    this.view = new Int32Array(this.buffer);
    Atomics.store(this.view, 0, initialValue);
  }

  get value() {
    return Atomics.load(this.view, 0);
  }

  increment() {
    return Atomics.add(this.view, 0, 1);
  }

  decrement() {
    return Atomics.sub(this.view, 0, 1);
  }
}

// 使用
const counter = new AtomicCounter(0);
counter.increment(); // 返回 0(旧值),当前值 1
counter.increment(); // 返回 1(旧值),当前值 2
console.log(counter.value); // 2

4.2 简单的锁

class SimpleLock {
  constructor() {
    this.buffer = new SharedArrayBuffer(4);
    this.view = new Int32Array(this.buffer);
    Atomics.store(this.view, 0, 0); // 0 = unlocked, 1 = locked
  }

  acquire() {
    // 自旋锁:不断尝试获取锁
    while (!this.tryAcquire()) {
      Atomics.wait(this.view, 0, 1, 100); // 等待 100ms
    }
  }

  tryAcquire() {
    return Atomics.compareExchange(this.view, 0, 0, 1) === 0;
  }

  release() {
    Atomics.store(this.view, 0, 0);
    Atomics.notify(this.view, 0, 1); // 通知一个等待的线程
  }
}

4.3 环形缓冲区

class RingBuffer {
  constructor(capacity = 1024) {
    this.capacity = capacity;
    this.buffer = new SharedArrayBuffer(capacity + 8); // +8 for pointers
    this.dataView = new Uint8Array(this.buffer, 8);
    this.pointerView = new Int32Array(this.buffer, 0, 2);
  }

  get readIndex() {
    return Atomics.load(this.pointerView, 0);
  }

  get writeIndex() {
    return Atomics.load(this.pointerView, 1);
  }

  push(data) {
    const writeIdx = this.writeIndex;
    const nextWriteIdx = (writeIdx + 1) % this.capacity;
    const readIdx = this.readIndex;

    if (nextWriteIdx === readIdx) {
      return false; // Buffer full
    }

    this.dataView[writeIdx] = data;
    Atomics.store(this.pointerView, 1, nextWriteIdx);
    return true;
  }

  pop() {
    const readIdx = this.readIndex;
    const writeIdx = this.writeIndex;

    if (readIdx === writeIdx) {
      return null; // Buffer empty
    }

    const data = this.dataView[readIdx];
    const nextReadIdx = (readIdx + 1) % this.capacity;
    Atomics.store(this.pointerView, 0, nextReadIdx);
    return data;
  }
}

五、面试高频考点

考点 1:原子操作的意义

原子操作保证在多线程环境中,操作不会被其他线程的操作分割。要么操作完全完成,要么完全不执行,不会出现"读到一半"的状态。这对于共享状态的数据一致性至关重要。

考点 2:CAS 操作的原理

Compare Exchange 首先读取当前值,与期望值比较。如果相等,则写入新值并返回旧值;如果不等,则不做修改并返回当前值。CAS 是无锁编程的基础,可以实现各种同步原语。

考点 3:SharedArrayBuffer 的安全机制

Spectre/Meltdown 漏洞利用 CPU 推测执行读取不该访问的内存。浏览器的缓解措施是 Cross-Origin Isolation,通过 COOP 和 COEP 头部确保页面隔离。


延展阅读