news 2026/4/23 9:24:52

JavaScript 的原子操作(Atomics):在多线程场景下避免数据竞态(Data Race)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript 的原子操作(Atomics):在多线程场景下避免数据竞态(Data Race)

JavaScript 的原子操作(Atomics):在多线程场景下避免数据竞态

随着现代Web应用日益复杂,对性能和响应速度的要求也越来越高。传统的单线程JavaScript模型虽然简单易用,但在处理计算密集型任务或需要并行处理大量数据时,其局限性日益凸显。Web Workers的出现,使得JavaScript能够在浏览器环境中实现真正的并行执行,将耗时操作从主线程剥离,从而避免UI阻塞。然而,并发编程也带来了新的挑战——数据竞态(Data Race)。当多个线程尝试同时访问和修改同一块共享内存时,如果不加以适当的同步控制,就可能导致不可预测的错误结果,这就是数据竞态。

JavaScript的Atomics对象正是为了解决这一核心问题而设计的。它提供了一组原子操作,用于安全地、无锁地访问和修改SharedArrayBuffer中的数据,从而在多线程环境下保证数据的一致性和正确性。

1. 并发编程的基石:Web Workers 与 SharedArrayBuffer

在深入Atomics之前,我们首先需要理解JavaScript实现并发编程的两个关键技术:Web Workers和SharedArrayBuffer

1.1 Web Workers:开启多线程之门

Web Workers允许我们在后台线程中运行JavaScript脚本,而不会阻塞用户界面。每个Worker都在一个独立的环境中运行,拥有自己的全局作用域,并通过postMessageonmessage事件与主线程进行通信。

// main.js (主线程) const worker = new Worker('worker.js'); worker.postMessage({ command: 'start' }); // 发送消息给Worker worker.onmessage = function(e) { console.log('主线程收到消息:', e.data); }; // worker.js (Worker线程) onmessage = function(e) { console.log('Worker收到消息:', e.data); // 执行一些耗时操作 postMessage({ result: '任务完成' }); // 发送消息回主线程 };

这种基于消息传递的通信方式虽然安全,但当需要频繁共享大量数据时,性能开销会比较大,因为每次传递数据都需要进行序列化和反序列化(或结构化克隆)。

1.2 SharedArrayBuffer:实现共享内存

为了解决消息传递的性能瓶颈,并实现更高效的数据共享,SharedArrayBuffer应运而生。SharedArrayBuffer是一种特殊的ArrayBuffer,它允许在不同的执行上下文(例如主线程和多个Web Worker线程)之间共享其底层的字节数据。与普通的ArrayBuffer不同,SharedArrayBuffer在传递给Worker时,不会被复制,而是共享同一个内存区域的引用。

// main.js (主线程) // 创建一个1KB的共享缓冲区 const sharedBuffer = new SharedArrayBuffer(1024); // 在共享缓冲区上创建一个32位整数视图 const sharedInt32Array = new Int32Array(sharedBuffer); // 将sharedBuffer传递给Worker const worker1 = new Worker('worker1.js'); const worker2 = new Worker('worker2.js'); worker1.postMessage(sharedBuffer); worker2.postMessage(sharedBuffer); console.log('主线程初始值:', sharedInt32Array[0]); // 0 // worker1.js onmessage = function(e) { const sharedBuffer = e.data; const sharedInt32Array = new Int32Array(sharedBuffer); // Worker 1 修改共享内存 sharedInt32Array[0] = 100; console.log('Worker 1 修改后:', sharedInt32Array[0]); }; // worker2.js onmessage = function(e) { const sharedBuffer = e.data; const sharedInt32Array = new Int32Array(sharedBuffer); // Worker 2 读取共享内存 setTimeout(() => { // 稍作延迟,让Worker 1有机会写入 console.log('Worker 2 读取到:', sharedInt32Array[0]); }, 50); };

通过SharedArrayBuffer,多个线程可以直接读写同一块内存区域,极大地提高了数据共享的效率。然而,这也引入了数据竞态的风险。

2. 理解数据竞态:并发编程的陷阱

数据竞态是指当两个或多个线程并发访问同一个共享内存位置,并且至少有一个访问是写入操作时,程序执行的结果依赖于这些访问发生的相对顺序。由于操作系统的调度不确定性,这种顺序无法保证,从而导致不可预测的行为。

2.1 经典案例:非原子计数器问题

考虑一个简单的场景:多个Worker线程需要对一个共享的计数器进行增量操作。如果不对操作进行同步,就会发生数据竞态。

// main.js (主线程) const sharedBuffer = new SharedArrayBuffer(4); // 4字节,用于存储一个Int32 const sharedCounter = new Int32Array(sharedBuffer); sharedCounter[0] = 0; // 初始计数器为0 const NUM_WORKERS = 5; const INCREMENTS_PER_WORKER = 100000; let completedWorkers = 0; console.log('初始计数器:', sharedCounter[0]); for (let i = 0; i < NUM_WORKERS; i++) { const worker = new Worker('counterWorker.js'); worker.postMessage(sharedBuffer); worker.onmessage = () => { completedWorkers++; if (completedWorkers === NUM_WORKERS) { console.log('最终计数器 (非原子):', sharedCounter[0]); // 理论上应该是 NUM_WORKERS * INCREMENTS_PER_WORKER = 5 * 100000 = 500000 // 但实际结果会小于这个值,且每次运行可能不同 } }; } // counterWorker.js (Worker线程) onmessage = function(e) { const sharedBuffer = e.data; const sharedCounter = new Int32Array(sharedBuffer); for (let i = 0; i < INCREMENTS_PER_WORKER; i++) { // 这是一个典型的读-改-写(Read-Modify-Write)操作 // 1. 读取当前值 // 2. 将值加1 // 3. 将新值写入 sharedCounter[0]++; // 这一行代码并非原子操作! } postMessage('done'); };

运行上述代码,你会发现最终的计数器值总是小于期望的500,000,并且每次运行的结果可能都不同。这是因为sharedCounter[0]++这个看似简单的操作,在底层实际上包含了三个步骤:

  1. 从内存中读取sharedCounter[0]的值。
  2. 将读取到的值加1。
  3. 将新值写回sharedCounter[0]

当两个Worker同时执行这个操作时,可能会发生以下情况:

  • Worker A 读取sharedCounter[0](值为X)。
  • Worker B 读取sharedCounter[0](值为X)。
  • Worker A 将X加1,得到X+1。
  • Worker B 将X加1,得到X+1。
  • Worker A 将X+1写入sharedCounter[0]
  • Worker B 将X+1写入sharedCounter[0]

最终结果是sharedCounter[0]只增加了1,而不是期望的2。这就是数据竞态导致的更新丢失(Lost Update)问题。

2.2 数据竞态的危害

数据竞态可能导致:

  • 结果不正确:如上面的计数器问题,计算结果与预期不符。
  • 程序崩溃:如果共享数据结构(如链表、树)在并发修改下损坏,可能导致后续操作读取无效数据或访问越界,从而引发运行时错误。
  • 难以调试:竞态条件往往是间歇性的,难以复现,给调试带来巨大挑战。

为了避免这些问题,我们需要使用原子操作来保证对共享内存的访问是安全的。

3. Atomics API 核心概念

Atomics对象提供了一组静态方法,用于在SharedArrayBuffer上执行原子操作。这些操作是不可中断的,这意味着它们要么完全执行,要么根本不执行,不会在中间被其他线程打断。这确保了在多线程环境中对共享数据的操作具有一致性和完整性。

Atomics操作的主要特点:

  • 原子性:操作是不可分割的,要么全部完成,要么全部不完成。
  • 操作对象:它不直接操作SharedArrayBuffer,而是操作基于SharedArrayBuffer创建的TypedArray视图(如Int32Array,Uint8Array等)。并且这些TypedArray视图的元素类型必须是整型,因为原子操作主要针对整数字节进行。
  • 顺序一致性Atomics操作提供了最强的内存顺序保证,即顺序一致性。这意味着所有线程看到的原子操作的顺序都是相同的,并且与程序的源代码顺序一致。这极大地简化了并发编程中的推理。

Atomics对象本身不能被构造,所有方法都是静态的。

// 示例:Atomics是一个静态对象 console.log(typeof Atomics); // "object" // new Atomics() 会报错

4. Atomics API 详解与代码示例

Atomics提供了一系列用于不同目的的原子操作。我们将逐一介绍这些方法,并提供代码示例。

4.1 原子算术操作:Atomics.add,Atomics.sub,Atomics.and,Atomics.or,Atomics.xor

这些方法原子地执行数学或位运算,并返回操作前的值。

  • Atomics.add(typedArray, index, value): 原子地将value加到typedArray[index]上,并返回typedArray[index]的旧值。
  • Atomics.sub(typedArray, index, value): 原子地从typedArray[index]中减去value,并返回typedArray[index]的旧值。
  • Atomics.and(typedArray, index, value): 原子地将typedArray[index]value进行按位AND操作,并返回typedArray[index]的旧值。
  • Atomics.or(typedArray, index, value): 原子地将typedArray[index]value进行按位OR操作,并返回typedArray[index]的旧值。
  • Atomics.xor(typedArray, index, value): 原子地将typedArray[index]value进行按位XOR操作,并返回typedArray[index]的旧值。

示例:使用Atomics.add解决计数器问题

我们来修改之前的计数器示例,使用Atomics.add来确保增量操作的原子性。

// main.js (主线程) const sharedBuffer = new SharedArrayBuffer(4); const sharedCounter = new Int32Array(sharedBuffer); sharedCounter[0] = 0; // 初始计数器为0 const NUM_WORKERS = 5; const INCREMENTS_PER_WORKER = 100000; let completedWorkers = 0; console.log('初始计数器:', sharedCounter[0]); for (let i = 0; i < NUM_WORKERS; i++) { const worker = new Worker('atomicCounterWorker.js'); worker.postMessage(sharedBuffer); worker.onmessage = () => { completedWorkers++; if (completedWorkers === NUM_WORKERS) { console.log('最终计数器 (原子操作):', sharedCounter[0]); // 理论上应该是 NUM_WORKERS * INCREMENTS_PER_WORKER = 5 * 100000 = 500000 // 这次会得到正确的结果 } }; } // atomicCounterWorker.js (Worker线程) onmessage = function(e) { const sharedBuffer = e.data; const sharedCounter = new Int32Array(sharedBuffer); for (let i = 0; i < INCREMENTS_PER_WORKER; i++) { // 使用 Atomics.add 替代 sharedCounter[0]++ // 这会原子地读取、加1、写入,不会被其他线程打断 Atomics.add(sharedCounter, 0, 1); } postMessage('done'); };

通过Atomics.add,每个Worker对sharedCounter[0]的增量操作都变成了原子操作。即使多个Worker同时尝试增加计数器,它们也会排队执行,确保每次操作都能正确更新计数器,从而得到预期的最终结果。

4.2 原子比较与交换:Atomics.compareExchange(CAS)

Atomics.compareExchange是一个非常强大的原子操作,它实现了“比较并交换”(Compare-And-Swap, CAS)语义。这是实现许多无锁(lock-free)数据结构和算法的基础。

  • Atomics.compareExchange(typedArray, index, expectedValue, replacementValue):
    • 检查typedArray[index]的值是否等于expectedValue
    • 如果相等,则原子地将typedArray[index]的值设置为replacementValue
    • 无论是否成功交换,都返回typedArray[index]的旧值(即在比较发生时的值)。

示例:基于CAS的自旋锁(Spinlock)

自旋锁是一种简单的互斥锁,当锁被占用时,尝试获取锁的线程会不断地“自旋”检查锁的状态,直到锁可用。

// main.js (主线程) - 用于启动Worker和观察结果 const sharedBuffer = new SharedArrayBuffer(8); // 4字节用于锁,4字节用于计数器 const sharedArray = new Int32Array(sharedBuffer); const LOCK_INDEX = 0; const COUNTER_INDEX = 1; sharedArray[LOCK_INDEX] = 0; // 锁状态:0 = 未锁定, 1 = 锁定 sharedArray[COUNTER_INDEX] = 0; // 共享计数器 const NUM_WORKERS = 3; const INCREMENTS_PER_WORKER = 100000; let completedWorkers = 0; console.log('初始计数器:', sharedArray[COUNTER_INDEX]); for (let i = 0; i < NUM_WORKERS; i++) { const worker = new Worker('spinlockWorker.js'); worker.postMessage(sharedBuffer); worker.onmessage = () => { completedWorkers++; if (completedWorkers === NUM_WORKERS) { console.log('最终计数器 (自旋锁):', sharedArray[COUNTER_INDEX]); // 期望值: 3 * 100000 = 300000 } }; } // spinlockWorker.js (Worker线程) const LOCK_INDEX = 0; const COUNTER_INDEX = 1; onmessage = function(e) { const sharedBuffer = e.data; const sharedArray = new Int32Array(sharedBuffer); function acquireLock(sharedArray, index) { let oldValue; do { // 尝试将锁从0 (未锁定) 设置为1 (锁定) // 如果成功,返回0;如果失败,返回其他值(通常是1) oldValue = Atomics.compareExchange(sharedArray, index, 0, 1); } while (oldValue !== 0); // 如果旧值不是0,说明锁已经被其他线程持有,继续自旋 } function releaseLock(sharedArray, index) { // 原子地将锁设置为0 (未锁定) Atomics.store(sharedArray, index, 0); // Atomics.store在这里是合适的,因为它只需要写入 } for (let i = 0; i < INCREMENTS_PER_WORKER; i++) { acquireLock(sharedArray, LOCK_INDEX); try { // 临界区:只有持有锁的线程才能执行 sharedArray[COUNTER_INDEX]++; // 这里可以是非原子操作,因为有锁保护 } finally { releaseLock(sharedArray, LOCK_INDEX); } } postMessage('done'); };

在这个例子中,acquireLock函数使用Atomics.compareExchange来尝试获取锁。只有当锁处于未锁定状态(sharedArray[LOCK_INDEX]为0)时,它才能成功将其设置为1并获得锁。如果锁已被占用,它会不断尝试,直到锁可用。releaseLock函数则简单地将锁状态设回0,释放锁。这确保了在任何给定时间只有一个Worker能够修改计数器。

4.3 原子加载与存储:Atomics.load,Atomics.store

虽然普通地读取typedArray[index]和写入typedArray[index] = value在JavaScript层面看起来是原子操作,但在某些底层硬件架构或编译器优化下,它们可能不是完全原子的,或者不能保证内存顺序。Atomics.loadAtomics.store提供了明确的原子性和内存顺序保证。

  • Atomics.load(typedArray, index): 原子地读取typedArray[index]的值。
  • Atomics.store(typedArray, index, value): 原子地将value写入typedArray[index]

它们的主要作用是确保内存可见性(Memory Visibility),即一个线程的写入操作对另一个线程是立即可见的,并且不会被编译器或CPU乱序执行。

示例:使用Atomics.storeAtomics.load确保标志位可见性

考虑一个线程设置一个标志,另一个线程等待这个标志。

// main.js (主线程) const sharedBuffer = new SharedArrayBuffer(4); const sharedFlag = new Int32Array(sharedBuffer); const FLAG_INDEX = 0; sharedFlag[FLAG_INDEX] = 0; // 0 = 未设置, 1 = 已设置 const producerWorker = new Worker('producer.js'); const consumerWorker = new Worker('consumer.js'); producerWorker.postMessage(sharedBuffer); consumerWorker.postMessage(sharedBuffer); // producer.js onmessage = function(e) { const sharedBuffer = e.data; const sharedFlag = new Int32Array(sharedBuffer); console.log('生产者: 生产数据...'); // 模拟生产数据耗时 setTimeout(() => { // 生产完成,设置标志位 Atomics.store(sharedFlag, FLAG_INDEX, 1); console.log('生产者: 标志位已设置。'); }, 100); }; // consumer.js onmessage = function(e) { const sharedBuffer = e.data; const sharedFlag = new Int32Array(sharedBuffer); console.log('消费者: 等待标志位...'); let flagValue = 0; // 不断检查标志位 while (flagValue === 0) { flagValue = Atomics.load(sharedFlag, FLAG_INDEX); // 为了避免忙循环导致CPU占用过高,可以稍作延迟 // 但在生产环境中,通常会使用 Atomics.wait/notify // console.log('消费者: 检查中...'); } console.log('消费者: 标志位已检测到,值为:', flagValue, '开始消费数据...'); };

虽然在这个简单的例子中,直接访问sharedFlag[FLAG_INDEX]可能也能工作,但Atomics.loadAtomics.store提供了更强的保证,特别是在复杂的内存模型和优化场景下。

4.4 原子交换:Atomics.exchange

Atomics.exchange原子地将一个新值写入指定位置,并返回该位置的旧值。

  • Atomics.exchange(typedArray, index, value): 原子地将value写入typedArray[index],并返回typedArray[index]的旧值。

这与Atomics.compareExchange不同,exchange不进行比较,总是写入新值。

示例:简单的状态交换

// main.js (主线程) const sharedBuffer = new SharedArrayBuffer(4); const sharedStatus = new Int32Array(sharedBuffer); const STATUS_INDEX = 0; sharedStatus[STATUS_INDEX] = 0; // 初始状态 const worker = new Worker('exchangeWorker.js'); worker.postMessage(sharedBuffer); setTimeout(() => { console.log('主线程在100ms后读取到的状态:', Atomics.load(sharedStatus, STATUS_INDEX)); }, 100); // exchangeWorker.js onmessage = function(e) { const sharedBuffer = e.data; const sharedStatus = new Int32Array(sharedBuffer); console.log('Worker: 初始状态:', Atomics.load(sharedStatus, STATUS_INDEX)); // 0 // 将状态从0交换为1,并获取旧状态 const oldStatus = Atomics.exchange(sharedStatus, STATUS_INDEX, 1); console.log('Worker: 交换后旧状态 (应为0):', oldStatus); // 0 console.log('Worker: 交换后新状态 (应为1):', Atomics.load(sharedStatus, STATUS_INDEX)); // 1 // 再次尝试交换,将状态从1交换为2 const oldStatus2 = Atomics.exchange(sharedStatus, STATUS_INDEX, 2); console.log('Worker: 再次交换后旧状态 (应为1):', oldStatus2); // 1 console.log('Worker: 再次交换后新状态 (应为2):', Atomics.load(sharedStatus, STATUS_INDEX)); // 2 };

Atomics.exchange常用于实现简单的状态机或获取并重置标志。

4.5 等待与唤醒:Atomics.wait,Atomics.notify

Atomics.waitAtomics.notify是实现线程间同步和协调的强大机制,类似于操作系统中的条件变量(Condition Variable)或信号量(Semaphore)。它们允许一个线程在某个条件满足之前阻塞等待,而另一个线程在条件满足时唤醒等待的线程。

  • Atomics.wait(typedArray, index, value, [timeout]):
    • 检查typedArray[index]的值是否等于value
    • 如果相等,则阻塞当前Worker线程,直到typedArray[index]的值不再是value,或者被Atomics.notify唤醒,或者达到timeout(可选,毫秒)。
    • 返回一个字符串,表示等待的结果:"ok"(被唤醒),"not-equal"(初始值就不等于value),"timed-out"(超时)。
  • Atomics.notify(typedArray, index, [count]):
    • 唤醒正在typedArray[index]上等待的一个或多个Worker线程。
    • count参数指定要唤醒的线程数量(默认为Infinity,唤醒所有等待的线程)。
    • 返回实际唤醒的线程数量。

重要提示Atomics.wait只能在Worker线程中使用,不能在主线程中使用,因为在主线程中阻塞会导致UI完全冻结。

示例:生产者-消费者模型

使用Atomics.waitAtomics.notify构建一个简单的生产者-消费者模型。

// main.js (主线程) const BUFFER_SIZE = 10; // 共享缓冲区:一个用于锁,一个用于计数器,其余用于数据 const sharedBuffer = new SharedArrayBuffer((2 + BUFFER_SIZE) * Int32Array.BYTES_PER_ELEMENT); const sharedArray = new Int32Array(sharedBuffer); const LOCK_INDEX = 0; const ITEM_COUNT_INDEX = 1; // 缓冲区中当前项目数量 const DATA_START_INDEX = 2; // 数据实际开始的索引 sharedArray[LOCK_INDEX] = 0; sharedArray[ITEM_COUNT_INDEX] = 0; const producerWorker = new Worker('producerConsumerProducer.js'); const consumerWorker = new Worker('producerConsumerConsumer.js'); producerWorker.postMessage(sharedBuffer); consumerWorker.postMessage(sharedBuffer); // producerConsumerProducer.js (生产者 Worker) const BUFFER_SIZE = 10; const LOCK_INDEX = 0; const ITEM_COUNT_INDEX = 1; const DATA_START_INDEX = 2; onmessage = function(e) { const sharedBuffer = e.data; const sharedArray = new Int32Array(sharedBuffer); let producedCount = 0; function produce() { if (producedCount >= 20) { // 生产20个项目后停止 console.log('生产者: 生产任务完成。'); return; } // 尝试获取锁 while (Atomics.compareExchange(sharedArray, LOCK_INDEX, 0, 1) !== 0) { // 如果锁被占用,等待直到被唤醒(或超时),然后重试 Atomics.wait(sharedArray, LOCK_INDEX, 1, 100); // 等待100ms或被notify } try { const currentItems = Atomics.load(sharedArray, ITEM_COUNT_INDEX); if (currentItems < BUFFER_SIZE) { // 缓冲区未满,可以生产 const item = Math.floor(Math.random() * 100); sharedArray[DATA_START_INDEX + currentItems] = item; Atomics.add(sharedArray, ITEM_COUNT_INDEX, 1); producedCount++; console.log(`生产者: 生产了 ${item}。当前缓冲区有 ${Atomics.load(sharedArray, ITEM_COUNT_INDEX)} 个项目。`); // 唤醒消费者,可能有消费者在等待 Atomics.notify(sharedArray, ITEM_COUNT_INDEX, 1); // 唤醒一个等待ITEM_COUNT_INDEX的线程 } else { console.log('生产者: 缓冲区已满,等待消费者消费...'); // 如果缓冲区满,释放锁并等待消费者消费 Atomics.store(sharedArray, LOCK_INDEX, 0); // 必须先释放锁 Atomics.wait(sharedArray, ITEM_COUNT_INDEX, BUFFER_SIZE, Infinity); // 等待ITEM_COUNT_INDEX值改变 // 再次尝试生产 setTimeout(produce, 0); // 立即再次尝试 return; } } finally { Atomics.store(sharedArray, LOCK_INDEX, 0); // 释放锁 } setTimeout(produce, Math.random() * 500); // 模拟生产间隔 } produce(); }; // producerConsumerConsumer.js (消费者 Worker) const BUFFER_SIZE = 10; const LOCK_INDEX = 0; const ITEM_COUNT_INDEX = 1; const DATA_START_INDEX = 2; onmessage = function(e) { const sharedBuffer = e.data; const sharedArray = new Int32Array(sharedBuffer); let consumedCount = 0; function consume() { if (consumedCount >= 20) { // 消费20个项目后停止 console.log('消费者: 消费任务完成。'); return; } // 尝试获取锁 while (Atomics.compareExchange(sharedArray, LOCK_INDEX, 0, 1) !== 0) { Atomics.wait(sharedArray, LOCK_INDEX, 1, 100); } try { const currentItems = Atomics.load(sharedArray, ITEM_COUNT_INDEX); if (currentItems > 0) { // 缓冲区非空,可以消费 const item = sharedArray[DATA_START_INDEX + currentItems - 1]; // 简单地从末尾取 Atomics.sub(sharedArray, ITEM_COUNT_INDEX, 1); consumedCount++; console.log(`消费者: 消费了 ${item}。当前缓冲区有 ${Atomics.load(sharedArray, ITEM_COUNT_INDEX)} 个项目。`); // 唤醒生产者,可能有生产者在等待 Atomics.notify(sharedArray, ITEM_COUNT_INDEX, 1); // 唤醒一个等待ITEM_COUNT_INDEX的线程 } else { console.log('消费者: 缓冲区为空,等待生产者生产...'); // 如果缓冲区空,释放锁并等待生产者生产 Atomics.store(sharedArray, LOCK_INDEX, 0); // 必须先释放锁 Atomics.wait(sharedArray, ITEM_COUNT_INDEX, 0, Infinity); // 等待ITEM_COUNT_INDEX值改变 // 再次尝试消费 setTimeout(consume, 0); return; } } finally { Atomics.store(sharedArray, LOCK_INDEX, 0); // 释放锁 } setTimeout(consume, Math.random() * 500); // 模拟消费间隔 } consume(); };

这个生产者-消费者模型结合了自旋锁(基于compareExchange)和条件变量(基于wait/notify)。当缓冲区满或空时,相应的线程会wait,直到被另一个线程notify唤醒,从而实现高效的线程协调。

4.6 其他操作:Atomics.isLockFree
  • Atomics.isLockFree(size):
    • 检查给定字节大小的原子操作是否可以由硬件以无锁(lock-free)方式执行。
    • 如果可以,返回true;否则返回false
    • 例如,Atomics.isLockFree(4)检查32位整数的原子操作是否是无锁的。
    • 在JavaScript中,Atomics操作本身就保证了原子性,这个方法更多是提供底层硬件能力的信息,对JavaScript开发者来说,原子操作的保证是语言层面的。
console.log('32位整数操作是否无锁:', Atomics.isLockFree(4)); // 通常返回 true console.log('64位整数操作是否无锁:', Atomics.isLockFree(8)); // 取决于平台,可能返回 true 或 false
4.7 Atomics API 概览

下表总结了Atomics对象的主要方法及其用途:

方法描述返回值适用场景
Atomics.add原子地加值并返回旧值typedArray[index]的旧值计数器、累加器
Atomics.sub原子地减值并返回旧值typedArray[index]的旧值计数器、资源消耗
Atomics.and原子地按位与并返回旧值typedArray[index]的旧值标志位操作、权限管理
Atomics.or原子地按位或并返回旧值typedArray[index]的旧值标志位设置
Atomics.xor原子地按位异或并返回旧值typedArray[index]的旧值标志位翻转、数据校验
Atomics.load原子地读取值typedArray[index]的当前值确保内存可见性、读取共享数据
Atomics.store原子地写入值value确保内存可见性、写入共享数据
Atomics.exchange原子地写入新值并返回旧值typedArray[index]的旧值状态机、交换变量
Atomics.compareExchange原子地比较并交换值 (CAS)typedArray[index]的旧值 (比较发生时的值)实现无锁算法、自旋锁、并发数据结构
Atomics.wait阻塞线程直到指定条件满足或被唤醒"ok","not-equal","timed-out"线程同步、条件变量、生产者-消费者模型中的等待
Atomics.notify唤醒正在等待指定条件的线程实际唤醒的线程数量线程同步、条件变量、生产者-消费者模型中的唤醒
Atomics.isLockFree检查给定大小的原子操作是否由硬件以无锁方式执行truefalse性能优化、底层硬件能力查询

5.Atomics在实际应用中的考量与高级模式

Atomics提供了底层的原子操作原语,为构建更复杂的并发结构奠定了基础。

5.1 性能与正确性的权衡

原子操作通常比非原子操作具有更高的开销,因为它们需要更强的内存同步和硬件指令支持。因此,在不需要原子性的场景下,应避免过度使用Atomics。然而,在共享内存和多线程并发访问的场景下,为了保证数据正确性和避免数据竞态,Atomics是不可或缺的。正确性永远是首要的。

5.2 浏览器和Node.js中的支持

SharedArrayBufferAtomicsAPI在现代浏览器和Node.js环境中都得到了广泛支持。值得注意的是,SharedArrayBuffer曾因Spectre安全漏洞被暂时禁用,后通过更严格的跨域隔离策略(COOP/COEP HTTP头)重新启用。在部署使用SharedArrayBufferAtomics的应用时,需要确保服务器配置了正确的HTTP响应头以启用这些特性。

5.3 构建高级同步原语

Atomics本身是底层工具,但它们可以用于构建更高级别的同步原语,例如:

  • 互斥锁(Mutex):上面自旋锁的例子就是一个简单的互斥锁。一个更完善的互斥锁会结合Atomics.waitAtomics.notify来避免忙等待,从而提高CPU利用率。
  • 信号量(Semaphore):可以用来控制对共享资源的并发访问数量。
  • 屏障(Barrier):确保所有参与线程都达到某个同步点后才能继续执行。

示例:基于Atomics.wait/notify的互斥锁

为了避免自旋锁的忙等待导致的CPU资源浪费,我们可以结合Atomics.waitAtomics.notify来实现一个更高效的互斥锁。

// main.js (主线程) - 启动Worker const sharedBuffer = new SharedArrayBuffer(8); // Lock (4 bytes), Counter (4 bytes) const sharedArray = new Int32Array(sharedBuffer); const LOCK_INDEX = 0; const COUNTER_INDEX = 1; sharedArray[LOCK_INDEX] = 0; // 0: unlocked, 1: locked sharedArray[COUNTER_INDEX] = 0; const NUM_WORKERS = 3; const INCREMENTS_PER_WORKER = 100000; let completedWorkers = 0; console.log('初始计数器:', sharedArray[COUNTER_INDEX]); for (let i = 0; i < NUM_WORKERS; i++) { const worker = new Worker('mutexWorker.js'); worker.postMessage(sharedBuffer); worker.onmessage = () => { completedWorkers++; if (completedWorkers === NUM_WORKERS) { console.log('最终计数器 (Mutex):', sharedArray[COUNTER_INDEX]); } }; } // mutexWorker.js (Worker线程) const LOCK_INDEX = 0; const COUNTER_INDEX = 1; onmessage = function(e) { const sharedBuffer = e.data; const sharedArray = new Int32Array(sharedBuffer); function acquireMutex(sharedArray, index) { // 尝试获取锁 while (Atomics.compareExchange(sharedArray, index, 0, 1) !== 0) { // 如果未能将锁从0变为1 (说明锁已被占用),则等待 // 这里我们等待在锁索引上,等待它的值从1(已锁定)变为0(未锁定) // 注意:wait的第三个参数是期望值,只有当前值等于这个期望值时才会阻塞 Atomics.wait(sharedArray, index, 1, Infinity); // 阻塞直到被notify或超时 } } function releaseMutex(sharedArray, index) { // 释放锁 Atomics.store(sharedArray, index, 0); // 将锁设置为0 (未锁定) // 唤醒一个可能正在等待的线程 Atomics.notify(sharedArray, index, 1); // 唤醒一个等待LOCK_INDEX的线程 } for (let i = 0; i < INCREMENTS_PER_WORKER; i++) { acquireMutex(sharedArray, LOCK_INDEX); try { sharedArray[COUNTER_INDEX]++; } finally { releaseMutex(sharedArray, LOCK_INDEX); } } postMessage('done'); };

这个Mutex实现比自旋锁更高效,因为它在锁不可用时会让线程进入休眠状态,而不是持续消耗CPU。

6. JavaScript并发编程的未来与挑战

AtomicsSharedArrayBuffer的引入,标志着JavaScript在并发编程能力上迈出了重要一步。它们为Web应用和Node.js服务提供了构建高性能、多线程解决方案的底层工具。

未来的发展可能会看到:

  • 更高级别的并发抽象库:基于AtomicsSharedArrayBuffer,出现更易用、更安全的并发数据结构和工具库,隐藏底层复杂性。
  • WebAssembly与Atomics的结合:WebAssembly线程模型与SharedArrayBufferAtomics无缝集成,允许C/C++/Rust等语言编写的高性能并发代码在Web上运行。
  • 更完善的内存模型理解:随着并发编程的普及,开发者需要更深入地理解内存模型、顺序一致性、内存屏障等概念,以避免难以察觉的并发bug。

挑战依然存在,主要是并发编程本身的复杂性。即使有了原子操作,死锁、活锁、饥饿等问题仍然可能发生,需要开发者具备严谨的并发思维和调试能力。

7. 共享内存与并发控制的基石

Atomics对象是JavaScript中处理共享内存和并发控制的核心工具。它提供了一套原子操作,确保了在多线程环境中对SharedArrayBuffer中数据的访问和修改是安全且可预测的,从而有效地避免了数据竞态。理解并熟练运用AtomicsAPI,是现代JavaScript开发者构建高性能、健壮并发应用程序的关键技能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/19 23:53:58

玩转Sobol参数灵敏度分析:MATLAB实战手册

基于sobol的全局参数灵敏度分析 可自行更改参数数目和目标函数matlab编程 注:因程序可下载&#xff0c;一经出&#xff0c;概不退换&#xff0c;敬请谅解。遇到十几个参数需要调优的仿真模型怎么办&#xff1f;Sobol全局灵敏度分析就是那个帮你揪出"关键先生"的神器。…

作者头像 李华
网站建设 2026/4/18 10:15:48

基于脉振高频电流注入的永磁同步电机无感FOC。 采用脉振高频电流注入法实现零低速下无感起动运行

基于脉振高频电流注入的永磁同步电机无感FOC。 采用脉振高频电流注入法实现零低速下无感起动运行&#xff0c;相比电压注入法可以省去电流反馈中的两个低通滤波器&#xff1b;相比高频电压注入&#xff0c;估计系统的稳定性不受电机定子电阻、电感变化以及注入信号频率的影响&a…

作者头像 李华
网站建设 2026/4/16 18:37:28

搞工业自动化的兄弟都懂,遇到扫码枪和PLC联机这事儿说简单也不简单。上个月刚在车间折腾完一套FX3U配得利捷扫码枪的方案,今天就把实战经验掏出来聊聊

三菱FX3U与扫码枪通讯程序(SL3U-4) 功能&#xff1a;使用三菱FX3U485BD板&#xff0c;使三菱PLC串口与扫码枪通讯。 实现扫码枪扫条码&#xff0c;反应灵敏&#xff0c;通讯稳定可靠。 其他扫码枪可以参考这个 器件&#xff1a;三菱FX3UPLC&#xff0c;三菱fx3u485BD&#xff0…

作者头像 李华
网站建设 2026/4/9 22:59:02

为什么你的PHP医疗数据备份总失败?4个被忽视的关键点

第一章&#xff1a;PHP医疗数据备份失败的根源解析在医疗信息系统中&#xff0c;数据完整性与可靠性至关重要。PHP作为后端常用语言&#xff0c;常被用于构建数据备份脚本&#xff0c;但在实际运行中&#xff0c;备份失败的情况屡见不鲜。深入分析其根本原因&#xff0c;有助于…

作者头像 李华
网站建设 2026/4/15 18:51:14

基于Simulink的风储联合调频与光伏变压减载仿真模型研究(附文献)

风储联合调频光伏变压减载simulink仿真模型 ①风机虚拟惯量调频 ②储能下垂控制联合调频&#xff1a;搭建了考虑储能充放电效率的含电池储能系统的电力系统一次调频模型 ③光伏变压减载 仿真速度快&#xff0c;波形好&#xff0c;附参考文献&#xff08;下图&#xff09;。 直接…

作者头像 李华
网站建设 2026/4/21 0:40:02

别再盲目压测了!构建精准纤维协程测试框架的4个关键指标

第一章&#xff1a;别再盲目压测了&#xff01;构建精准纤维协程测试框架的4个关键指标在高并发系统中&#xff0c;纤维协程&#xff08;Fiber/Coroutine&#xff09;已成为提升吞吐量的核心手段。然而&#xff0c;许多团队仍依赖传统压测方式&#xff0c;导致测试结果失真、资…

作者头像 李华