为何看似严谨的SharedArrayBuffer同步通信机制会出现数据损坏?
首先得给你点个赞,能把问题压缩到80行的复现案例,排查起来真的方便多了!咱们先拆解下你的代码逻辑,看看哪里漏了原子性保障,才会出现这些“不可能”的错误。
核心问题1:非原子操作的内存写入导致数据不一致
你在主线程写入响应数据的时候,犯了一个容易忽略的关键错误:只有Atomics命名空间下的操作是原子且具备内存可见性的,而Uint8Array.set()这类普通数组操作完全不具备这些特性。
看主线程这段代码:
Atomics.store(sbuf.vi32, 1, rui8.length); sbuf.vui8.set(rui8, 8); // 这步是非原子操作! Atomics.store(sbuf.vi32, 0, 1); Atomics.notify(sbuf.vi32, 0);
你以为先写长度、再写数据、最后发信号是严格顺序执行的,但JavaScript引擎或底层CPU可能会对非原子操作做指令重排,或者主线程还没写完整个vui8数组,Worker就已经被Atomics.notify()唤醒,开始读取数据了。这时候Worker读到的vui8内容是不完整的,自然会出现JSON解析失败的情况。
核心问题2:cooldown阶段的等待逻辑存在竞态条件
再看Worker里的cooldown等待代码:
if (m.cooldown > 0) { Atomics.wait(sbuf.vi32, 0, 0, m.cooldown); }
你觉得这段时间里vi32[0]不会被修改,但实际上主线程是异步处理Worker的postMessage请求的——当Worker还在cooldown等待的时候,主线程可能已经收到了Worker下一次循环发送的sync消息,并且直接修改了vi32[0]的值,导致Atomics.wait()提前被唤醒,而不是超时。
这就完美解释了为什么你加了错误判断后,会收到Atomics.wait返回非timed-out的异常。
修复方案:给所有关键操作加上原子性屏障
要解决这两个问题,咱们需要把所有涉及共享内存读写的关键步骤都用原子操作保护起来,并且严格控制同步顺序:
1. 主线程写入数据时,保证内存可见性
把非原子的vui8.set()放到原子操作之前,并且用Atomics操作触发内存屏障,确保数据写入完成后再让Worker读取:
// 主线程修改后的代码 const rui8 = new TextEncoder().encode(JSON.stringify(m)); const sbuf = workers[workerName].sbuf; // 先写数据,再写长度(确保Worker读到长度时,数据已经完全写入) sbuf.vui8.set(rui8, 8); // 用Atomics.store触发内存屏障,保证之前的写入同步到共享内存 Atomics.store(sbuf.vi32, 1, rui8.length); // 最后发信号通知Worker Atomics.store(sbuf.vi32, 0, 1); Atomics.notify(sbuf.vi32, 0);
另外,读取数据长度的时候,Worker也要用Atomics.load()来保证读到的是最新值:
// Worker里读取长度的代码修改为 let data_length = Atomics.load(sbuf.vi32, 1);
2. 调整Worker循环逻辑,避免cooldown阶段的竞态
你需要保证Worker在cooldown等待期间,不会让主线程提前处理下一个sync请求。最简单的方式是确保下一次postMessage是在cooldown结束后才发送:
// Worker修改后的完整循环 let pl = 0; while (true) { postMessage({ type: "sync", payload: pl++ }); Atomics.wait(sbuf.vi32, 0, 0); Atomics.store(sbuf.vi32, 0, 0); let data_length = Atomics.load(sbuf.vi32, 1); let data = new TextDecoder().decode(sbuf.vui8.slice(8, 8 + data_length)); let m; try { m = JSON.parse(data); } catch (e) { throw new Error("How is this possible? Bad JSON:" + data); } if (m.cooldown > 0) { let rv = Atomics.wait(sbuf.vi32, 0, 0, m.cooldown); if (rv !== 'timed-out') { throw new Error("How is this possible? Atomics.wait returned: " + rv); } } }
这样修改后,Worker在cooldown等待时,不会发送新的sync消息,主线程也就不会提前修改vi32[0],Atomics.wait()就能正常超时了。
3. 额外优化:用标记位确保数据完整性
更稳妥的方式是在共享内存里加一个“数据就绪”的标记位,主线程写完所有数据后再把标记位设为1,Worker先等待这个标记位,再读取数据:
// 主线程最后加一步 Atomics.store(sbuf.vi32, 2, 1); // 标记数据就绪 Atomics.notify(sbuf.vi32, 2); // Worker里先等待标记位 Atomics.wait(sbuf.vi32, 2, 0); Atomics.store(sbuf.vi32, 2, 0); // 重置标记位
这种方式能彻底避免数据未写完就被读取的问题。
验证修复效果
把上面的修改应用到你的代码里,再运行测试,应该就不会再出现JSON解析失败和Atomics.wait异常的情况了。本质上,这些问题都是因为你对SharedArrayBuffer的原子性保障理解不够全面——只有Atomics相关操作能保证内存操作的原子性和有序性,其他普通数组操作都不具备这些特性。
备注:内容来源于stack exchange,提问作者Gillespie

