在WebGL中实现线程模拟的SharedArrayBuffer
解读
Unity WebGL 导出后,JavaScript 运行在主线程,且WebGL 线程模型与原生线程完全不同。
国内面试问“线程模拟的 SharedArrayBuffer”,并不是让你真的开线程,而是考察:
- 是否知道 WebGL 导出禁用 pthread(Unity 官方文档明确提示);
- 能否用 SharedArrayBuffer + Atomics 在 主线程与 WebWorker 之间 做“伪并发”数据同步;
- 是否能把 Unity C# 端对多线程的访问模式(如双缓冲、环形队列)无损迁移到 JavaScript 层,并保证 Unity 主循环不掉帧;
- 是否理解国内 跨域隔离(COOP/COEP) 政策,SharedArrayBuffer 必须 HTTPS + 响应头 才能启用,否则线上会直接抛异常。
一句话:在 无真线程、无共享内存 的 WebGL 沙箱里,用 官方允许的 SharedArrayBuffer 把“后台计算”搬到 WebWorker,并 对上层 C# 提供线程安全的假象。
知识点
-
Unity WebGL 导出限制
- 编译选项
-pthread被 Emscripten 强制关闭; System.Threading.Thread在构建期直接报错;UnityJobSystem的IJobParallelFor会被降级成主循环顺序执行。
- 编译选项
-
SharedArrayBuffer 复活史
- 2018 年因 Spectre 被禁用;
- 2021 年国内主流浏览器(Chrome 92、Edge 92、Firefox 79)重新放开,前提是响应头
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp - 字节、腾讯、网易的 WebGL 小游戏上线流程里,运维必须手动加头,否则 SAB 实例化失败。
-
Atomics 四字诀
Atomics.load/store保证 单操作原子;Atomics.wait/wake实现 阻塞同步原语(模拟 mutex、sem);Atomics.compareExchange做 无锁环形队列 的 head/tail 竞争;- 禁止在 主线程 调用
Atomics.wait,会抛 TypeError,只能在 Worker 里阻塞。
-
Unity 与 JavaScript 双向通道
jslib插件里把 SAB 地址通过_malloc映射到 C#IntPtr;- C# 端用
Marshal.Copy读写,避免每次跨语言拷贝; - Worker 计算完后通过
postMessage发 0 拷贝信号,Unity 在下一帧Update里消费。
-
性能红线
- 主线程 每帧访问 SAB 次数 < 2000 次(Chrome 实测 4 ms 以上就会掉 60 FPS);
- Worker 计算任务粒度 > 5 ms 才能抵消通信开销;
- 移动端微信、抖音内置浏览器 iOS 15 以下不支持 SAB,必须 优雅降级 到单线程。
答案
-
开启跨域隔离
在 CDN 或 Nginx 层加响应头,否则后续代码都跑不通。 -
C# 端定义共享结构
// ThreadSafeData.cs
[StructLayout(LayoutKind.Sequential, Size = 64)]
public struct ThreadSafeData {
public int head;
public int tail;
public unsafe fixed float matrix[12]; // 48 B
}
- jslib 插件导出 SAB
// WebGLWorker.jslib
mergeInto(LibraryManager.library, {
WebGLWorker_Init: function (byteLength) {
const sab = new SharedArrayBuffer(byteLength);
// 存到全局供 Worker 使用
window.webGLSab = sab;
// 返回指针给 C#
return _malloc(byteLength);
},
WebGLWorker_GetSABPtr: function () {
return window.webGLSab ? window.webGLSab : 0;
}
});
- 实例化 Worker 并建立协议
// worker.js
let sab, view;
self.onmessage = e => {
if (e.data.sab) {
sab = e.data.sab;
view = new Int32Array(sab);
}
// 后台计算,例如蒙皮矩阵
while (Atomics.load(view, 0) !== 1) {
// 用 compareExchange 抢锁
if (Atomics.compareExchange(view, 0, 0, 2) === 0) {
for (let i = 0; i < 12; ++i) {
view[i + 2] = Math.sin(performance.now() * 0.001 + i);
}
Atomics.store(view, 0, 0); // 释放锁
Atomics.notify(view, 0, 1); // 唤醒主线程
}
}
};
- Unity 主循环消费
void Update() {
unsafe {
int* ptr = (int*)sabPtr;
// 非阻塞检测
if (Interlocked.CompareExchange(ref ptr[0], 1, 0) == 0) {
float* mat = (float*)(ptr + 2);
// 直接拷到 MaterialPropertyBlock,0 拷贝
mpb.SetMatrix("unity_MatrixVP", new Matrix4x4(
new Vector4(mat[0], mat[1], mat[2], mat[3]),
...
));
Graphics.DrawMeshInstanced(mesh, 0, mat, matrices, 1023, mpb);
// 归还锁
Interlocked.Exchange(ref ptr[0], 0);
}
}
}
- 国内上线 checklist
- 运维确认 HTTPS + COOP/COEP;
- 代码层检测
'SharedArrayBuffer' in window,不支持就回退到协程版; - 性能监控埋点:每帧
performance.now()差值 > 16 ms 自动上报; - 微信小游戏需 转码为 wasm2js,SAB 仍可用,但包体增大 30%,必须做分包加载。
拓展思考
-
无 SAB 场景
政务内网、教育平板常禁用 COOP,此时可用 Transferable ArrayBuffer 每帧 postMessage,代价是 一次内存拷贝,实测 1 万个 float 耗时 0.8 ms,可接受。 -
Unity 2023 的 WebGL Threads
官方实验包已把 pthread 映射到 Web Worker + SAB,但 国内 CDN 缓存命中率低,首包 15 MB 以上,不适合微信小游戏,建议 继续用自研方案。 -
GPU 端并行替代
把计算搬到 ComputeShader,通过WebGL2.0的EXT_disjoint_timer_query测耗时,绕过 CPU 线程限制;但 WebGL1.0 机型占比 18%(国内安卓 7),需 回退到 SAB 方案。 -
安全合规
工信部 2024 年新规要求 WebGL 应用上报“共享内存使用声明”,否则应用商店下架;SharedArrayBuffer 必须在用户协议里显式告知,面试时可主动提到,体现合规意识。