解密JavaScript多线程的「安全锁」:Atomics原子操作入门指南

215 阅读2分钟

咖啡店的库存危机

凌晨三点的咖啡店,两位值班员工同时发现最后一杯冰美式被下单了。收银系统显示库存为1,小美和小帅几乎同时点击了「确认出单」按钮——库存瞬间变成了-1!这个经典的多线程问题,正是JavaScript中的Atomics要解决的难题。

为什么需要原子操作?

当多个Web Worker线程操作同一个SharedArrayBuffer共享内存时,常规运算就像不设防的收银台:

// 危险的非原子操作示例
sharedArray[0] += 1; 

这条看似简单的指令在底层可能被拆分为多个步骤,当多个线程同时操作时,最终结果可能丢失部分修改。原子操作(Atomic operations) 就像收银台的叫号系统,确保每个操作不可分割、完整执行

Atomics核心武器库

1. 四则运算三剑客

// 原子加法(返回旧值)
let old = Atomics.add(intArray, 0, 5); 

// 原子减法
Atomics.sub(intArray, 0, 3);

// 原子交换值
let prev = Atomics.exchange(intArray, 0, 100);

2. 位运算三神器

// 按位与
Atomics.and(intArray, 0, 0b1111);

// 按位或 
Atomics.or(intArray, 0, 0b1000);

// 按位异或
Atomics.xor(intArray, 0, 0b1010);

3. 条件操作王牌

// 比较并交换(CAS操作)
Atomics.compareExchange(intArray, 0, 100, 200);

当原值等于预期值(100)时,才会更新为新值(200),返回原始值。这是实现锁机制的核心方法。

线程协作的「信号灯」

1. 等待-通知机制

// 主线程
Atomics.store(intArray, 0, 123);
Atomics.notify(intArray, 0); // 唤醒等待的Worker

// Worker线程
Atomics.wait(intArray, 0, 0); // 进入休眠直到被唤醒

注意:主线程中禁止调用wait(),大多数浏览器会抛出错误

2. 异步等待新方式

Atomics.waitAsync(intArray, 0, 0)
  .then(() => {
    console.log('数据已更新!');
  });

实战:多线程计数器

即使10个线程同时操作,最终结果必定是10000,完美避免数据竞争!

// 主线程
const worker = new Worker('counter.js');
const sharedBuffer = new SharedArrayBuffer(4);
const intArray = new Int32Array(sharedBuffer);

// 启动10个线程同时计数
for(let i=0; i<10; i++) {
  new Worker('./counter.js').postMessage(sharedBuffer);
}

// counter.js
self.onmessage = function(e) {
  const intArray = new Int32Array(e.data);
  for(let i=0; i<1000; i++) {
    Atomics.add(intArray, 0, 1); // 原子递增
  }
};

性能优化秘诀

使用Atomics.isLockFree(n)判断操作是否直接使用硬件原子指令:

Atomics.isLockFree(1); // true (8/16位)
Atomics.isLockFree(2); // true (32位)
Atomics.isLockFree(3); // false 
Atomics.isLockFree(4); // true (64位)

返回true时表示无需软件锁,性能更高。

多线程时代的必备技能

Atomics对象就像JavaScript多线程世界的交通警察,确保共享内存操作的有序进行。虽然Web Worker的使用场景仍有限制,但在音视频处理、科学计算等高性能领域,掌握原子操作将成为你突破性能瓶颈的关键钥匙。

🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧