导读:本期聚焦于南京网站建设创作的《JavaScript 的 Atomics 对象在 SharedArrayBuffer 多线程编程中扮演什么角色?》,敬请观看详情。当多个 Web Worker 同时读写同一个 SharedArrayBuffer 时,为什么数据会莫名错乱?答案藏在 JavaScript 的内存模型里。Atomics 对象提供了一套原子操作方法,能保证对共享内存的读取、写入和修改是不可分割的整体,从根本上避免竞态条件。本文将深入讲解 Atomics 的核心原理,包括原子读写、原子交换、比较交换以及等待通知机制,并结合生产者消费者模型的完整代码示例,演示如何安全地在多线程间同步数据。同时还会分析浏览器对 SharedArrayBuffer 的安全限制要求,帮助你在实际项目中正确落地这套多线程方案。

在 Web Worker 已经普及的今天,前端开发者对多线程编程不再陌生。但 Worker 之间的通信依赖 postMessage 消息传递,数据需要经过序列化和反序列化,遇到大量数据高频交互的场景性能就会成为瓶颈。SharedArrayBuffer 的出现让多个 Worker 可以直接共享同一块内存,但共享内存带来一个经典问题:如果两个线程同时修改同一个位置,数据就会出错。这个时候,Atomics 对象就是解决问题的核心工具。

JavaScript 的 Atomics 对象在 SharedArrayBuffer 多线程编程中扮演什么角色?

为什么共享内存必须依赖 Atomics

要理解 Atomics 的必要性,先看一个反例。假设两个线程同时对同一个 Int32Array 的同一位置执行 arr[0] = arr[0] + 1,这个语句在 JavaScript 层面是一条语句,但在 CPU 层面实际分成了读取、加一、写回三个步骤。线程 A 刚读完值 5,还没写回,线程 B 也读到了 5,各自加一后都写回 6,最终结果只加了一次。这就是典型的竞态条件。

除了竞态问题,还有一个容易被忽视的点:JavaScript 引擎和 CPU 都可能对指令重排序。普通的对 TypedArray 的读写操作,编译器可以随意调整顺序以优化性能,而在多线程环境下这种重排可能让另一个线程观察到不符合预期的状态。Atomics 提供的所有操作都保证了两个特性:一是原子性,操作不可分割,其他线程要么看到操作前的值,要么看到操作后的值;二是有序性,Atomics 操作之间不会发生重排序问题,配合 Atomics.loadAtomics.store 可以在共享内存上建立同步点。

需要强调的是,Atomics 的所有方法只接受 Int8Array、Uint8Array、Int16Array、Uint16Array、Int32Array、Uint32Array、BigInt64Array、BigUint64Array 这些整数类型的 TypedArray,不支持 Float32Array 和普通数组。原因在于 CPU 原子指令通常只覆盖整数操作,浮点数无法保证硬件级别的原子性。

Atomics 的核心方法详解

Atomics 的 API 可以分成三组。第一组是基本的原子读写和修改类操作。Atomics.store(ta, index, value)Atomics.load(ta, index) 分别负责原子写入和读取;Atomics.addAtomics.subAtomics.andAtomics.orAtomics.xor 则是原子化的位运算和加减运算,它们都会返回修改前的旧值,这个设计在实现无锁计数器时非常实用。

第二组是交换类操作。Atomics.exchange(ta, index, value) 会把新值写入并返回旧值,整个过程原子完成。Atomics.compareExchange(ta, index, expected, replacement) 则更精细:只有当当前位置的值等于 expected 时才写入 replacement,否则不做任何修改,无论成败都返回当前实际值。它是实现自旋锁、无锁算法的基石,下面是一个简单的自旋锁实现:

// 使用 Atomics.compareExchange 实现自旋锁
// lock 值为 0 表示未锁定,1 表示已锁定
const sab = new SharedArrayBuffer(4);
const lock = new Int32Array(sab);

function acquireLock() {
  // 不断尝试把 0 改成 1,成功说明抢到了锁
  while (Atomics.compareExchange(lock, 0, 0, 1) !== 0) {
    // 抢锁失败,短暂等待后重试,避免空转浪费 CPU
    Atomics.wait(lock, 0, 1, 10);
  }
}

function releaseLock() {
  // 释放锁并唤醒一个等待线程
  Atomics.store(lock, 0, 0);
  Atomics.notify(lock, 0, 1);
}

第三组是等待通知机制,也就是 Atomics.waitAtomics.notify。这一组方法让 JavaScript 具备了类似操作系统层面的条件变量能力。Atomics.wait(ta, index, expected, timeout) 会检查指定位置的值是否等于 expected,如果相等就阻塞当前线程直到被唤醒或超时;如果不相等则立即返回字符串 not-equal。Atomics.notify(ta, index, count) 用来唤醒在该位置上等待的线程。要注意的是,wait 只能在 Worker 线程中调用,主线程调用会直接抛出异常,因为阻塞主线程会导致页面卡死。

实战:用 Atomics 实现生产者消费者模型

下面通过一个完整的例子演示 Atomics 在实际多线程协作中的用法。场景是主线程往共享缓冲区写任务,Worker 线程消费任务。为了简单起见,用一个位置作为信号量来协调双方。

首先是主线程的代码:

<script>
  const sab = new SharedArrayBuffer(8);
  const control = new Int32Array(sab);
  control[0] = 0; // 0 表示空闲,1 表示有数据

  const worker = new Worker('consumer.js');
  worker.postMessage(sab);

  function produce(value) {
    // 等待消费者处理完上一条数据
    Atomics.wait(control, 0, 1);
    control[1] = value;             // 写入数据
    Atomics.store(control, 0, 1);   // 原子标记为有数据
    Atomics.notify(control, 0);     // 唤醒消费者
  }

  produce(42);
</script>

然后是 consumer.js 中 Worker 端的代码:

// consumer.js - Worker 消费者
let control;

self.onmessage = (e) => {
  control = new Int32Array(e.data);
  consumeLoop();
};

function consumeLoop() {
  while (true) {
    // 等待数据就绪,若 control[0] 不为 0 则立即返回 not-equal
    const result = Atomics.wait(control, 0, 0);
    if (result === 'ok' || result === 'not-equal') {
      const value = Atomics.load(control, 1); // 读取数据
      Atomics.store(control, 0, 0);           // 标记为已消费
      Atomics.notify(control, 0);             // 通知生产者可以继续
      console.log('消费到数据:', value);
    }
  }
}

这个例子体现了 Atomics 的典型用法:数据的可见性通过 Atomics.storeAtomics.load 保证,线程间的协调通过 waitnotify 完成。相比 postMessage 每次传递都要拷贝数据,共享内存加原子操作的方式在高频小数据交互下吞吐量可以提升数倍,特别适合音频处理、图像编解码、科学计算这类需要大量数据交换的场景。

使用时的限制与注意事项

SharedArrayBuffer 的可用性有一个绕不开的前提:出于 Spectre 漏洞的安全考虑,主流浏览器要求页面必须处于跨域隔离状态才能使用它。具体来说,服务器需要返回两个响应头:Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp,可以通过 crossOriginIsolated 这个全局变量检测是否满足条件。本地开发时这个限制经常让人卡住,值得提前配置好。

另外几点实践建议:一是能用整数就用整数,Atomics 不支持浮点类型,如果必须传浮点数可以用 ArrayBuffer 和 Float64Array 共享同一块内存,通过整数区域做控制信号,浮点区域做数据载体;二是不要滥用 wait 的超时参数做轮询,优先依赖 notify 精确唤醒;三是 Node.js 环境同样支持这套 API,worker_threads 模块配合 SharedArrayBuffer 的用法和浏览器端完全一致,代码可以两边复用。

总结一下,Atomics 在 SharedArrayBuffer 体系中扮演的角色就是秩序的维护者。共享内存解决了性能问题,而 Atomics 通过原子性和可见性保证解决了正确性问题,二者缺一不可。掌握这套机制,就掌握了在 JavaScript 中做真正意义上多线程共享内存编程的钥匙。

AtomicsSharedArrayBufferWeb Worker修改时间:2026-09-15 09:10:34

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/57175.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。