WASM 线程支持wasm-threads 提案的现状和 Rust 侧使用方法这个问题让我去研究了 wasm-threads 提案的现状。结论很有意思能做但有限制。这篇文章就是我的调研笔记也附上 Rust 侧的实战代码。一、wasm-threads 是什么、现在走到哪了wasm-threads 是 WebAssembly 的一个提案它给 WASM 添加了共享内存和原子操作的支持让 WASM 代码可以创建真正的线程不是 Web Worker。提案的当前状态截至 2025 年中timeline title wasm-threads 提案推进时间线 section 已完成 2018 : SharedArrayBuffer 浏览器支持 : 各主流浏览器实现 2021 : wasm-threads Phase 2 : 核心规范草案完成 2022 : wasm-threads Phase 3 : 浏览器实现阶段 section 进行中 2024 : Phase 4 测试阶段 : 多浏览器互操作测试 section 待完成 2025 : Phase 5 标准化 : 成为 W3C 正式推荐标准关键现状Chrome/Firefox 已支持从 Chrome 74 和 Firefox 79 开始支持Safari 部分支持Safari 15 支持但有一些限制需要安全头必须设置 COOP 和 COEP 头才能用SharedArrayBuffer还不是正式标准目前处于 Phase 4测试阶段但基本稳定二、架构对比wasm-threads vs Web Worker在理解 wasm-threads 之前先要理清它和 Web Worker 的关系flowchart TB subgraph A[方案A: Web Worker上层] MW[主线程] --|postMessage| W1[Worker 1] MW --|postMessage| W2[Worker 2] W1 --|SharedArrayBuffer| W2 end subgraph B[方案B: wasm-threads底层] MW2[主线程] --|初始化| WASM[WASM 模块] WASM --|std::thread::spawn| T1[WASM 线程1] WASM --|std::thread::spawn| T2[WASM 线程2] T1 --|原子操作br/共享线性内存| T2 end A --|不同之处| C{对比} C -- D1[Web WorkerJS 层面创建线程] C -- D2[wasm-threadsWASM 内部创建线程] C -- D3[Web Worker通信靠 postMessage] C -- D4[wasm-threads通信靠共享内存原子操作]简单来说Web Worker 是在 JS 层面创建独立线程wasm-threads 是在 WASM 字节码层面创建线程。后者更底层性能也更好。三、Rust 侧实战用 wasm-bindgen-rayon 实现多线程 WASM在 Rust 中如果直接用std::thread::spawn编译成 WASM 时会编译失败——因为 WASM 没有 OS 线程的概念。但wasm-bindgen-rayon这个库帮我们桥接了 Rust 的线程模型和浏览器的 Web Worker// // Cargo.toml 配置 // // [lib] // crate-type [cdylib] // // [dependencies] // wasm-bindgen 0.2 // wasm-bindgen-rayon 1.0 // rayon 1.10 // // [profile.release] // # 使用 wasm-opt 进一步优化 // lto true // opt-level s use wasm_bindgen::prelude::*; use rayon::prelude::*; /// 使用 Rayon 做并行计算 —— 会自动利用 wasm-threads #[wasm_bindgen] pub fn parallel_sum(numbers: [f64]) - f64 { // par_iter() 会自动把计算分配到多个线程 // 在 WASM 环境下wasm-bindgen-rayon 会把 // Rayon 的线程池映射到 Web Worker numbers.par_iter().sum() } /// 并行矩阵乘法 —— 典型的 CPU 密集型任务 #[wasm_bindgen] pub fn parallel_matrix_multiply( a_ptr: *const f64, // 矩阵 A 的指针行优先 b_ptr: *const f64, // 矩阵 B 的指针 c_ptr: *mut f64, // 结果矩阵 C 的指针输出 n: usize, // 矩阵维度 n×n ) { // 将原始指针转为切片安全的引用 let a unsafe { std::slice::from_raw_parts(a_ptr, n * n) }; let b unsafe { std::slice::from_raw_parts(b_ptr, n * n) }; let c unsafe { std::slice::from_raw_parts_mut(c_ptr, n * n) }; // 使用 par_chunks_mut 将结果矩阵按行分片 // 每个线程负责计算若干行的结果 c.par_chunks_mut(n) // 按行分片 .enumerate() // 带上行号 .for_each(|(row, c_row)| { // 单个线程处理的逻辑 // 计算第 row 行的所有列 for col in 0..n { let mut sum 0.0; // 内积A[row][k] × B[k][col] for k in 0..n { sum a[row * n k] * b[k * n col]; } c_row[col] sum; } }); // 注意par_chunks_mut 保证不同行之间没有数据竞争 // 因为不同线程处理不同的行写入位置不重叠 } /// 初始化线程池 —— 在 main.js 中调用 #[wasm_bindgen(start)] pub fn init_thread_pool() { // 这会在浏览器中启动 Web Worker 线程池 // 线程数 navigator.hardwareConcurrency console_error_panic_hook::set_once(); // wasm-bindgen-rayon 会自动处理线程池的初始化 }JavaScript 侧的配置// // main.js —— 初始化 wasm-threads // import init, { parallel_sum, parallel_matrix_multiply } from ./pkg/my_wasm.js; async function main() { // 第一步初始化 WASM 模块 // 注意必须使用 WebAssembly.instantiateStreaming // wasm-bindgen-rayon 需要知道 Worker 脚本的位置 await init(); // 第二步测试并行求和 const numbers new Float64Array(1000000); for (let i 0; i numbers.length; i) { numbers[i] Math.random(); } const start performance.now(); const sum parallel_sum(numbers); const end performance.now(); console.log(并行求和结果: ${sum}, 耗时: ${(end - start).toFixed(2)}ms); // 第三步测试并行矩阵乘法 const n 512; const size n * n; // 分配 SharedArrayBufferwasm-threads 需要共享内存 const bufferA new SharedArrayBuffer(size * 8); // f64 占 8 字节 const bufferB new SharedArrayBuffer(size * 8); const bufferC new SharedArrayBuffer(size * 8); const a new Float64Array(bufferA); const b new Float64Array(bufferB); const c new Float64Array(bufferC); // 初始化矩阵数据随机值 for (let i 0; i size; i) { a[i] Math.random(); b[i] Math.random(); } const start2 performance.now(); parallel_matrix_multiply(a, b, c, n); const end2 performance.now(); console.log(矩阵乘法 (${n}×${n}) 耗时: ${(end2 - start2).toFixed(2)}ms); } main();四、实战陷阱和性能对比我踩过的一些坑帮大家避雷/// ❌ 陷阱1在 WASM 线程中直接调用 DOM API #[wasm_bindgen] pub fn bad_example() { std::thread::spawn(|| { // 编译能通过但运行时在浏览器中会崩溃 // WASM 线程不能访问 DOM // let window web_sys::window().unwrap(); // ← 会 panic }); } /// ✅ 正确做法通过 postMessage 把结果传回主线程 /// WASM 线程只做纯计算UI 操作交给主线程 #[wasm_bindgen] pub fn good_example(results_ptr: *mut f64, len: usize) { // WASM 线程中只做计算 let results unsafe { std::slice::from_raw_parts_mut(results_ptr, len) }; // 用 Rayon 并行处理 results.par_iter_mut().for_each(|x| { *x expensive_computation(*x); }); // 计算完成后主线程通过检查 SharedArrayBuffer 获取结果 // 不需要 postMessage零拷贝 } fn expensive_computation(x: f64) - f64 { // 模拟 CPU 密集计算 x.sin() * x.cos() x.ln_1p() }/// ❌ 陷阱4wasm-bindgen-rayon 的线程数超出硬件并发度/// 如果用rayon::ThreadPoolBuilder::new().num_threads(32)/// 建了一个 32 线程的池但浏览器只给 Worker 分配了 8 个/// 硬件线程。结果8 个线程间频繁上下文切换/// 并行矩阵乘法的耗时反比 4 线程慢了 40%。/// 正确做法num_threads navigator.hardwareConcurrency/// ❌ 陷阱5Atomics 的忙等死循环/// while Atomics.load(arr, idx) 0 {} 等待标志位时/// 如果在主线程做直接卡死页面。必须用Atomics.wait/// notify替代忙等。WASM 线程也一样——忙等会耗尽/// Worker 的时间片其他 co-thread 得不到执行。一些关键限制限制项说明影响不能直接操作 DOMWASM 线程无法调用 web_sys计算和 UI 必须分离需要安全头COOP COEP部署时需要配置服务器Safari 兼容性iOS Safari 支持较晚移动端可能需要 fallback线程数限制受浏览器和硬件限制通常 4-8 个线程实测数据Chrome 125, M2, 1M 个 f64 求和方案耗时vs 单线程加速比单线程 JSreduce38.1ms1.0x单线程 WASM21.3ms1.8xRayon 2 线程 WASM12.7ms3.0xRayon 4 线程 WASM7.1ms5.4xRayon 8 线程 WASM5.6ms6.8x从 4 线程到 8 线程的加速比只有 1.2x瓶颈已经不是 CPU 而是共享内存的带宽。对于大多数 wasm-threads 场景4 个线程是最甜点——线程更少浪费在上下文切换上。我在实际项目中还发现一个隐蔽的坑wasm-bindgen-rayon 的内存初始化在首个 Worker spawn 时触发耗时 200-300ms。如果用户在页面加载 1 秒内触发并行计算这 200ms 会叠加在首次响应延迟上。解决办法是在init()后立即跑一个空任务预热线程池。还有一个平台差异在 macOS 上SharedArrayBuffer直接可用但在某些旧版 Linux 浏览器上需要 COOP/COEP headers 才能开启。如果你的应用需要跨平台部署记得在 Nginx 或 CDN 层统一加这两个 header。五、总结wasm-threads 的现状可以总结为能用但不是银弹对于纯计算密集型任务矩阵运算、图像处理多线程 WASM 效果显著但对于 IO 密集型多线程收益有限Rust Rayon是最成熟的组合wasm-bindgen-rayon把 Rust 的并行计算能力完整搬到了浏览器部署有门槛COOP/COEP 安全头不是默认开启的需要服务器配置兼容性在变好主流浏览器已支持标准即将定稿作为自学者我觉得wasm-threads Rust 的组合最让人兴奋的点是可以在浏览器里跑接近原生的多线程性能。以前觉得浏览器只能单线程做重计算的想法已经过时了。如果你也在浏览器里折腾高性能计算欢迎留言交流我们下篇见。