1)Atomics 与 SharedArrayBuffer
概念
- SharedArrayBuffer(SAB) :可被多个执行体(主线程/Worker)同时访问的内存缓冲区。它不复制数据,而是多方共享同一块内存。
- Atomics :在共享内存 上提供一组原子读写、算术、位运算、交换、以及"futex 风格"等待/唤醒的 API,确保并发下的内存可见性与操作不可分割性。
原理
- JS 的普通操作并不保证对共享内存的原子性 (不可分割)与顺序可见性(别的线程何时能看到你的写入)。
Atomics.*
在SharedArrayBuffer
上的 Int32Array/BigInt64Array 视图上执行,提供内存序保障(类似 CPU 原子指令 + 内存栅栏),避免"读旧值""丢写""乱序可见"等并发问题。
对比
ArrayBuffer
:只能单线程访问或拷贝给别人(会复制或转移所有权),无并行共享。SharedArrayBuffer
:真正并发共享 ,因此需要Atomics
来做同步 与互斥。- 高层抽象(比如消息通道
postMessage
)适合低频大块传输;SAB+Atomics 更适合高频、低延迟、细粒度的共享与同步(例如音视频帧环形缓冲区、计数器、锁/条件变量等)。
实践(小结)
- 没有 Atomics 的 SAB 常导致资源争用 :多线程同时读/改/写同一位置,会出现竞态。
- 用 Atomics 把关键步骤"锁住"或"原子化",再配合
wait/notify
做阻塞/唤醒式同步,就能写出稳定的并发代码。
拓展
- 可用
Atomics
自行实现 spinlock 、ticket lock 、信号量 、环形队列 、有界缓冲 、读写锁 等原语。 - 与
WebCodecs
、OffscreenCanvas
、WebAssembly
配合,能搭建高性能音视频管线、图像处理流水线。
潜在问题
- 只有 Worker 可以调用阻塞式
Atomics.wait
(主线程禁止阻塞)。 - 浏览器端使用 SAB 需要 crossOriginIsolated(COOP/COEP 头或等效策略)。
- 原子操作频繁也有成本:要平衡锁粒度 与争用。
2)SharedArrayBuffer:四个 Worker 访问同一 SAB 的"资源争用"演示
2.1 无原子操作:会丢写(错误示范)
ini
<!-- demo-race.html:演示丢写,需在 crossOriginIsolated 环境中运行 -->
<script>
// 1) 准备共享内存:仅 4 字节(一个 32 位整数)
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
view[0] = 0; // 共享计数器
// 2) Worker 脚本(用 Blob URL 内联,便于单文件运行)
const workerCode = `
self.onmessage = async (e) => {
const { sab, rounds } = e.data;
const view = new Int32Array(sab);
// 不使用 Atomics:典型的"读-改-写"竞态
for (let i = 0; i < rounds; i++) {
const v = view[0]; // A: 读
// 模拟计算耗时,增大竞态概率
for (let j = 0; j < 50; j++) {}
view[0] = v + 1; // B: 写(可能覆盖他人写入)
}
postMessage('done');
};
`;
const workerUrl = URL.createObjectURL(new Blob([workerCode], {type: 'text/javascript'}));
// 3) 启动 4 个 worker,每个自增 N 次
const N = 10000;
const workers = Array.from({length: 4}, () => new Worker(workerUrl));
let done = 0;
workers.forEach(w => {
w.onmessage = () => {
if (++done === workers.length) {
console.log('期望值 =', 4*N, '实际值 =', view[0]); // 实际值通常 < 期望值
}
};
w.postMessage({ sab, rounds: N });
});
</script>
说明 :多个 Worker 在 view[0]
上做"读-改-写",没有互斥或原子化,发生丢写(比如两个线程都读取了旧值 5,然后分别写回 6 → 6,等价于只+1一次)。
2.2 使用 Atomics.add
修复丢写(正确示范)
ini
<!-- demo-atomic-add.html:用 Atomics 消除丢写 -->
<script>
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
view[0] = 0;
const workerCode = `
self.onmessage = async (e) => {
const { sab, rounds } = e.data;
const view = new Int32Array(sab);
for (let i = 0; i < rounds; i++) {
// 原子加:硬件级不可分割 + 内存序保障
Atomics.add(view, 0, 1);
}
postMessage('done');
};
`;
const workerUrl = URL.createObjectURL(new Blob([workerCode], {type: 'text/javascript'}));
const N = 10000;
const workers = Array.from({length: 4}, () => new Worker(workerUrl));
let done = 0;
workers.forEach(w => {
w.onmessage = () => {
if (++done === workers.length) {
console.log('期望值 =', 4*N, '实际值 =', view[0]); // === 期望值
}
};
w.postMessage({ sab, rounds: N });
});
</script>
3)原子操作基础
3.1 算术与位运算:add/sub/or/and/xor
使用场景
- 计数/配额 :
add
/sub
- 多标志位 :
or
设置位,and
清位,xor
翻转位 - 资源争用:用位标志表示"资源是否被占用/脏页/就绪"等
示例:用位运算管理 4 个"资源占用"位,并原子自增计数
scss
<script>
// 布局:index 0 = 计数器;index 1 = 标志位(低 4 位对应 4 个资源占用)
const sab = new SharedArrayBuffer(8);
const i32 = new Int32Array(sab);
const COUNTER = 0, FLAGS = 1;
// 设置第 k 位(占用资源 k)
function occupy(k) {
const mask = 1 << k;
Atomics.or(i32, FLAGS, mask); // 原子置位
}
// 清除第 k 位(释放资源 k)
function release(k) {
const mask = ~(1 << k);
Atomics.and(i32, FLAGS, mask); // 原子清位
}
// 查询第 k 位
function isOccupied(k) {
return (Atomics.load(i32, FLAGS) & (1 << k)) !== 0;
}
// 原子自增计数(比如处理任务数)
function inc() {
return Atomics.add(i32, COUNTER, 1) + 1; // 返回自增后的值
}
// ------ 演示 ------
occupy(0); // 占用资源0
console.log('flags after occupy(0)=', i32[FLAGS].toString(2));
console.log('isOccupied(0)=', isOccupied(0));
release(0);
console.log('flags after release(0)=', i32[FLAGS].toString(2));
console.log('inc() ->', inc()); // 1
</script>
3.2 原子的读/写:load
/ store
Atomics.load(view, idx)
:从共享内存原子读取,带必要内存序(保证之前的写入对当前可见)。Atomics.store(view, idx, value)
:原子写入并带发布语义(之后的读取者按顺序可见)。
示例:标志位 + 数据写入的"先写数据、再置就绪标志"
xml
<script>
// index 0: READY 标志;index 1: 数据
const sab = new SharedArrayBuffer(8);
const i32 = new Int32Array(sab);
const READY = 0, DATA = 1;
// 生产者(Worker 内常见)
function producerWrite(v) {
Atomics.store(i32, DATA, v); // ① 先写数据
Atomics.store(i32, READY, 1); // ② 再置就绪(发布)
// 消费者用 load/或 wait 看到 READY==1 时,保证能看到数据 v
}
// 消费者
function consumerRead() {
if (Atomics.load(i32, READY) === 1) {
const v = Atomics.load(i32, DATA);
console.log('read =', v);
}
}
producerWrite(42);
consumerRead(); // 输出 42
</script>
3.3 原子交换:exchange
/ compareExchange
exchange
:把索引处的值替换为新值,并返回旧值(常用于交换标志 、获取旧状态)。compareExchange
(CAS):若当前值等于期望值,则写入新值并返回旧值;否则不写,返回实际旧值。用来实现无锁算法 、自旋锁 、once 初始化等。
示例 A:exchange
实现"获取并清空"通知标志
xml
<script>
const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);
function notifyOnce() {
Atomics.store(i32, 0, 1); // 置 1:有通知
}
function consumeNotify() {
const prev = Atomics.exchange(i32, 0, 0); // 写回0,返回旧值
if (prev === 1) console.log('收到一次通知并已清空');
}
notifyOnce();
consumeNotify(); // 收到一次通知并已清空
</script>
示例 B:compareExchange
实现"最大值更新"(无锁)
xml
<script>
const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);
i32[0] = 10; // 初始最大值
function updateMax(candidate) {
while (true) {
const cur = Atomics.load(i32, 0);
if (candidate <= cur) return cur; // 不需要更新
// 仅当 cur 仍是当前值时,写入 candidate
const old = Atomics.compareExchange(i32, 0, cur, candidate);
if (old === cur) return candidate; // 成功
// 否则被别人抢先更新,循环重试
}
}
console.log(updateMax(15)); // 15
console.log(updateMax(12)); // 15(不会倒退)
</script>
3.4 原子的 "futex" 操作与加锁:wait
/ notify
Atomics.wait(view, idx, expected[, timeout])
:阻塞 当前 Worker,直到view[idx]
不再等于expected
或超时被唤醒(仅 Worker 可用!)。Atomics.notify(view, idx[, count])
:唤醒在该地址上等待的 count 个线程(默认唤醒全部)。
示例:4 个 Worker 依次取得锁并按顺序执行
思路:用一个共享变量 turn
指示当前轮到的线程 ID(0..3)。每个 Worker:
1)在 turn
不等于自己 ID 时调用 wait
挂起;
2)轮到自己时执行任务;3)将 turn
改为下一个 ID,并 notify
唤醒其他等待者。
ini
<!-- demo-turn.html:依次执行(0 → 1 → 2 → 3 → 循环) -->
<script>
const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);
const TURN = 0;
Atomics.store(i32, TURN, 0); // 从 0 号开始
const workerCode = `
self.onmessage = async (e) => {
const { sab, id, rounds } = e.data;
const i32 = new Int32Array(sab);
const TURN = 0;
for (let r = 0; r < rounds; r++) {
// 1) 等待轮到自己
while (Atomics.load(i32, TURN) !== id) {
// 等待值在当前值上发生变化
Atomics.wait(i32, TURN, Atomics.load(i32, TURN), 100); // 可设超时避免永久睡眠
}
// 2) 执行临界区(此处仅打印模拟)
// (在真实任务中,这里可访问共享数据结构)
postMessage(`Worker ${id}: run #${r}`);
// 3) 交接"令牌"给下一个
const next = (id + 1) % 4;
Atomics.store(i32, TURN, next); // 更新 turn
Atomics.notify(i32, TURN, 1); // 唤醒一个等待者
}
postMessage(`Worker ${id}: done`);
};
`;
const url = URL.createObjectURL(new Blob([workerCode], {type:'text/javascript'}));
const workers = Array.from({length:4}, (_,id)=>new Worker(url));
workers.forEach((w,id)=>{
w.onmessage = (e)=> console.log(e.data);
w.postMessage({ sab, id, rounds: 3 });
});
</script>
4)"视频播放"示例(OffscreenCanvas + SAB 环形缓冲 + Atomics.wait/notify)
目标:搭一个"解码/生产者 → 渲染/消费者"的双 Worker 管线:
- 解码 Worker 把帧像素(这里用程序生成假帧 代替真实解码)写入 SAB 环形缓冲。
- 渲染 Worker 拿到
OffscreenCanvas
,在 Worker 内Atomics.wait
阻塞等待有帧可用,唤醒后从 SAB 读帧并绘制。- 主线程只负责创建 canvas、传参、显示 UI,不做阻塞等待。
✅ 重点:演示 有界缓冲区 (避免无限增长)、等待/唤醒 (低 CPU 占用)、无复制共享(SAB)。
⚠️ 要求:在 crossOriginIsolated 环境运行(见文末部署说明)。
ini
<!-- video-sab.html:双 Worker 视频模拟播放(单文件版) -->
<canvas id="screen" width="320" height="180" style="border:1px solid #ccc"></canvas>
<button id="start">Start</button>
<button id="stop">Stop</button>
<script>
(() => {
// ========================
// 1) 配置环形缓冲区参数
// ========================
const WIDTH = 320, HEIGHT = 180, BYTES_PER_PIXEL = 4;
const FRAME_BYTES = WIDTH * HEIGHT * BYTES_PER_PIXEL;
const RING_CAP = 8; // 帧缓冲容量(越大越抗抖;越小越省内存)
// 控制区布局(Int32)
// [0] writeIndex 生产者写入位置(0..RING_CAP-1)
// [1] readIndex 消费者读取位置
// [2] available 当前可用帧数量(0..RING_CAP)
// [3] running 1=运行,0=停止(供协程优雅退出)
const CTRL_INTS = 4;
const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CTRL_INTS);
const ctrl = new Int32Array(controlSAB);
ctrl[0] = 0; ctrl[1] = 0; ctrl[2] = 0; ctrl[3] = 0;
// 帧区(共享像素),总大小 = CAP * FRAME_BYTES
const framesSAB = new SharedArrayBuffer(RING_CAP * FRAME_BYTES);
// ========================
// 2) Worker 代码(内联 Blob)
// ========================
const decoderCode = `
// 生产者:把"合成帧"写入环形缓冲,并 Atomics.notify 唤醒渲染线程
self.onmessage = (e) => {
const { controlSAB, framesSAB, width, height, cap } = e.data;
const ctrl = new Int32Array(controlSAB);
const bytes = new Uint8ClampedArray(framesSAB);
const FRAME_BYTES = width * height * 4;
function produceOne(t) {
// 1) 如果环形缓冲已满,等待消费者消耗
while (Atomics.load(ctrl, 2) === cap) {
// 在 available 索引(2)上等待值变化
Atomics.wait(ctrl, 2, cap, 50); // 50ms 超时避免死等
if (Atomics.load(ctrl, 3) === 0) return false; // 被要求停止
}
// 2) 写入一帧到 writeIndex
const wi = Atomics.load(ctrl, 0);
const base = wi * FRAME_BYTES;
// ------ 生成一个简单动态图案(渐变+移动方块)------
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = base + (y * width + x) * 4;
// 背景:横向渐变
bytes[i+0] = (x + t) % 256; // R
bytes[i+1] = (y*2) % 256; // G
bytes[i+2] = (255 - x) % 256; // B
bytes[i+3] = 255; // A
// 叠加一个随时间移动的白色小方块
const s = 32;
const cx = (t*2) % (width - s);
const cy = (t) % (height - s);
if (x >= cx && x < cx+s && y >= cy && y < cy+s) {
bytes[i+0] = 255; bytes[i+1] = 255; bytes[i+2] = 255;
}
}
}
// 3) 提交一帧:writeIndex 向前,available++
Atomics.store(ctrl, 0, (wi + 1) % cap);
Atomics.add(ctrl, 2, 1);
// 唤醒在 available 上等待的消费者
Atomics.notify(ctrl, 2, 1);
return true;
}
// 主循环:~60 FPS 生产
let t = 0;
const produceLoop = () => {
if (Atomics.load(ctrl, 3) === 0) return; // 停止
produceOne(t++);
setTimeout(produceLoop, 16);
};
produceLoop();
};
`;
const rendererCode = `
// 消费者:在 OffscreenCanvas 中阻塞等待有帧可用,再绘制
self.onmessage = (e) => {
const { canvas, controlSAB, framesSAB, width, height, cap } = e.data;
const off = canvas;
const ctx = off.getContext('2d');
const ctrl = new Int32Array(controlSAB);
const bytes = new Uint8ClampedArray(framesSAB);
const FRAME_BYTES = width * height * 4;
function drawOne() {
// 1) 若无可用帧,则阻塞等待 available 变化
while (Atomics.load(ctrl, 2) === 0) {
Atomics.wait(ctrl, 2, 0, 100); // 100ms 超时防止永久睡眠
if (Atomics.load(ctrl, 3) === 0) return false; // 停止
}
// 2) 取出一帧
const ri = Atomics.load(ctrl, 1);
const base = ri * FRAME_BYTES;
// 为了兼容性,复制到非共享缓冲再创建 ImageData(一些浏览器对 SAB-backed ImageData 有限制)
const local = new Uint8ClampedArray(FRAME_BYTES);
local.set(bytes.subarray(base, base + FRAME_BYTES));
const img = new ImageData(local, width, height);
ctx.putImageData(img, 0, 0);
// 3) 消费完成:readIndex 前移,available--
Atomics.store(ctrl, 1, (ri + 1) % cap);
Atomics.sub(ctrl, 2, 1);
// 唤醒可能在等待"缓冲不满"的生产者
Atomics.notify(ctrl, 2, 1);
return true;
}
// 渲染循环:尽量快地消费(可加节流/时间戳对齐)
const loop = () => {
if (Atomics.load(ctrl, 3) === 0) return;
drawOne();
// 这里选择 requestAnimationFrame 风格节奏:~60fps
setTimeout(loop, 16);
};
loop();
};
`;
const decoderURL = URL.createObjectURL(new Blob([decoderCode], {type:'text/javascript'}));
const rendererURL = URL.createObjectURL(new Blob([rendererCode], {type:'text/javascript'}));
// ========================
// 3) 主线程:创建 Worker 与 OffscreenCanvas
// ========================
const canvas = document.getElementById('screen');
const startBtn = document.getElementById('start');
const stopBtn = document.getElementById('stop');
let decoder, renderer;
startBtn.onclick = () => {
if (Atomics.load(ctrl, 3) === 1) return; // 已运行
Atomics.store(ctrl, 3, 1); // running = 1
decoder = new Worker(decoderURL);
renderer = new Worker(rendererURL);
// 把 canvas 交给渲染 Worker
const offscreen = canvas.transferControlToOffscreen();
// 传入共享内存与参数(注意把 OffscreenCanvas 置于 transferable 中)
renderer.postMessage({
canvas: offscreen, controlSAB, framesSAB,
width: WIDTH, height: HEIGHT, cap: RING_CAP
}, [offscreen]);
decoder.postMessage({
controlSAB, framesSAB, width: WIDTH, height: HEIGHT, cap: RING_CAP
});
};
stopBtn.onclick = () => {
if (Atomics.load(ctrl, 3) === 0) return;
Atomics.store(ctrl, 3, 0); // 请求停止
Atomics.notify(ctrl, 2, 10); // 唤醒阻塞的 wait
decoder && decoder.terminate();
renderer && renderer.terminate();
decoder = renderer = null;
// 重置索引
Atomics.store(ctrl, 0, 0);
Atomics.store(ctrl, 1, 0);
Atomics.store(ctrl, 2, 0);
};
})();
</script>
你会看到:点击 Start 后,canvas 中出现平滑变化的"视频"画面(程序合成帧)。这证明了:
- 不复制像素(共享 SAB)→ 低延迟;
Atomics.wait/notify
在 Worker 内阻塞/唤醒 → 低 CPU 占用;- 环形缓冲(
available
、readIndex
、writeIndex
)→ 平衡生产/消费速率。
5)拓展与实践建议
- 接入真实视频 :用
WebCodecs
在解码 Worker 中解码EncodedVideoChunk
,把VideoFrame
映射/拷到 SAB(或转成 RGBA),其余流程相同。 - 音视频同步 :在控制区携带 时间戳 与 时钟偏移 ,渲染端按 wall-clock(
performance.now()
)或AudioContext
时钟对齐。 - 多生产者/多消费者 :把
available
拆为空槽信号量 与已填充信号量,或用 ticket lock 控制写入/读取原子性。 - 帧丢弃策略 :渲染端若落后,可原子地把
readIndex
前跳至最新,丢弃陈旧帧,保持实时性。
6)潜在问题与排错清单
-
Atomics.wait
只能在 Worker 调用 :主线程会抛异常。主线程宜用postMessage
/事件/RAF 协调。 -
crossOriginIsolated 要求(浏览器中使用 SAB 必备):
-
响应头需包含:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
-
或其它等效策略确保
window.crossOriginIsolated === true
。 -
你可在本地用任意静态服务器加这两行头部(如 Nginx/Express/Cloudflare Pages 等)再访问
video-sab.html
。
-
-
忙等 vs 阻塞 :若发现 CPU 飙高,检查是否错误地用 while 自旋;应使用
Atomics.wait/notify
。 -
数据对齐 :
Atomics
只能作用于Int32Array
/BigInt64Array
视图的元素(4/8 字节对齐)。 -
内存越界 :环形缓冲的
base + FRAME_BYTES
计算要严谨,最好封装成函数并加断言。 -
兼容性 :
OffscreenCanvas
、WebCodecs
在部分环境可能需要开关或降级方案(可退化为主线程绘制 + 非阻塞轮询)。