A std::thread + std::sync replacement for wasm32 with proper async integration.
This crate provides a unified threading API and synchronization primitives that work across both WebAssembly and native platforms. In practice, you can treat it as a cross-platform replacement for much of std::thread plus key std::sync primitives. Unlike similar crates, it's designed from the ground up to handle the async realities of browser environments.
Alongside thread APIs, this crate includes WebAssembly-safe synchronization primitives:
MutexRwLockCondvarSpinlockmpscchannels
These APIs are usable on their own; you do not need to spawn threads with this crate to use
Mutex, RwLock, Condvar, or mpsc.
These primitives adapt their behavior to the runtime:
- Native: uses thread parking for efficient blocking
- WASM worker: uses
Atomics.wait-based blocking when available - WASM main thread: falls back to non-blocking/spin strategies to avoid panics
use wasm_safe_thread::Mutex;
let data = Mutex::new(41);
*data.lock_sync() += 1;
assert_eq!(*data.lock_sync(), 42);use wasm_safe_thread::mpsc::channel;
let (tx, rx) = channel();
tx.send_sync(5).unwrap();
assert_eq!(rx.recv_sync().unwrap(), 5);In addition to synchronization primitives, this crate provides a std::thread-like API:
spawn(), Builder, JoinHandle, park(), Thread::unpark(), thread locals, and spawn hooks.
wasm_thread is a popular crate that aims to closely replicate std::thread on wasm targets. This section compares design goals and practical tradeoffs.
wasm_safe_thread: async-first, unified API that works identically on native and wasm32, playing well with the browser event loop.wasm_thread: highstd::threadcompatibility with minimal changes to existing codebases (wasm32 only; native usesstd::threaddirectly).
| Feature | wasm_safe_thread | wasm_thread |
|---|---|---|
| Native support | Unified API (same code runs on native and wasm) | Re-exports std::thread::* on native |
| Node.js support | Yes, via worker_threads |
Browser only |
| Event loop integration | yield_to_event_loop_async() for cooperative scheduling |
No equivalent |
| Spawn hooks | Global hooks that run at thread start | Not available |
| Parking primitives | park()/unpark() on wasm workers |
Not implemented |
| Scoped threads | Not implemented | scope() allows borrowing non-'static data |
| std compatibility | Custom Thread/ThreadId (similar API) |
Re-exports std::thread::{Thread, ThreadId} |
| Worker scripts | Inline JS via wasm_bindgen(inline_js) |
External JS files; es_modules feature for module workers |
| wasm-pack targets | ES modules (web) only |
web and no-modules via feature flag |
| Dependencies | wasm-bindgen, js-sys, continue | web-sys (many features), futures crate |
| Thread handle | thread() returns &Thread |
thread() is unimplemented (panics) |
Both crates provide:
spawn()andBuilderfor thread creationjoin()(blocking) andjoin_async()(async) for waiting on threadsis_finished()for non-blocking completion checks- Thread naming via
Builder::name()
- Main-thread blocking: both crates must avoid blocking APIs on the browser main thread;
join_async()is the safe path. - Spawn timing: wasm workers only run after the main thread yields back to the event loop.
- Worker spawning model:
wasm_threadproxies worker spawning through the main thread;wasm_safe_threadspawns directly (simpler, but different model).
Result passing:
wasm_safe_threaduses its built-inmpscchannels with asyncrecv_async()wasm_threadusesArc<Packet<UnsafeCell>>with a customSignalprimitive andWakerlist
Async waiting:
wasm_safe_threadwraps JavaScript Promises viawasm-bindgen-futures::JsFuturewasm_threadimplementsfutures::future::poll_fnwith manualWakertracking
Choose wasm_safe_thread when:
- You need Node.js support (wasm_thread is browser-only)
- You want identical behavior on native and wasm (e.g., for testing)
- You need park/unpark synchronization primitives
- You need spawn hooks for initialization (logging, tracing, etc.)
- You prefer fewer dependencies and no external JS files
- You want an actively developed library with responsive issue/PR handling
Choose wasm_thread when:
- You need scoped threads for borrowing non-
'staticdata - You want maximum compatibility with
std::threadtypes - You need
no-moduleswasm-pack target support
Replace use std::thread with use wasm_safe_thread as thread:
# if cfg!(target_arch="wasm32") { return; } //join() not reliable here
use wasm_safe_thread as thread;
// Spawn a thread
let handle = thread::spawn(|| {
println!("Hello from a worker!");
42
});
// Wait for the thread to complete
// Synchronous join (works on native and some browser context - but not reliably!)
let result = handle.join().unwrap();
assert_eq!(result, 42);use wasm_safe_thread::{spawn, spawn_named, Builder};
// Simple spawn
let handle = spawn(|| "result");
// Convenience function for named threads
let handle = spawn_named("my-worker", || "result").unwrap();
// Builder pattern for more options
let handle = Builder::new()
.name("my-worker".to_string())
.spawn(|| "result")
.unwrap();# if cfg!(target_arch="wasm32") { return; } //join() not reliable here
use wasm_safe_thread::spawn;
// Synchronous join (works on native and some browser context - but not reliably!)
let handle = spawn(|| 42);
let result = handle.join().unwrap();
assert_eq!(result, 42);
// Non-blocking check
let handle = spawn(|| 42);
if handle.is_finished() {
// Thread completed
}
# drop(handle);For async contexts, use join_async:
// In an async context (e.g., with wasm_bindgen_futures::spawn_local)
let result = handle.join_async().await.unwrap();use wasm_safe_thread::{current, sleep, yield_now};
use std::time::Duration;
// Get current thread
let thread = current();
println!("Thread: {:?}", thread.name());
// Sleep
sleep(Duration::from_millis(10));
// Yield to scheduler
yield_now();Park/unpark works from background threads:
# if cfg!(target_arch="wasm32") { return } //join not reliable on wasm
use wasm_safe_thread::{spawn, park, park_timeout};
use std::time::Duration;
let handle = spawn(|| {
// Park/unpark (from background threads)
park_timeout(Duration::from_millis(10)); // Wait with timeout
});
handle.thread().unpark(); // Wake parked thread
handle.join().unwrap(); // join() is not reliable on wasm and should be avoided# #[cfg(not(target_arch = "wasm32"))]
# fn main() {
use wasm_safe_thread::yield_to_event_loop_async;
// Yield to browser event loop (works on native too)
# wasm_safe_thread::test_executor::spawn(async {
yield_to_event_loop_async().await;
# });
# }
# #[cfg(target_arch = "wasm32")]
# fn main() {} // JsFuture is !Send, tested separately via wasm_bindgen_testuse wasm_safe_thread::thread_local;
use std::cell::RefCell;
thread_local! {
static COUNTER: RefCell<u32> = RefCell::new(0);
}
COUNTER.with(|c| {
*c.borrow_mut() += 1;
});Register callbacks that run when any thread starts:
use wasm_safe_thread::{register_spawn_hook, remove_spawn_hook, clear_spawn_hooks};
// Register a hook
register_spawn_hook("my-hook", || {
println!("Thread starting!");
});
// Hooks run in registration order, before the thread's main function
// Remove specific hook
remove_spawn_hook("my-hook");
// Clear all hooks
clear_spawn_hooks();When spawning async tasks inside a worker thread using wasm_bindgen_futures::spawn_local,
you must notify the runtime so the worker waits for tasks to complete before exiting:
use wasm_safe_thread::{task_begin, task_finished};
task_begin();
wasm_bindgen_futures::spawn_local(async {
// ... async work ...
task_finished();
});These functions are no-ops on native platforms, so you can use them unconditionally in cross-platform code.
The browser main thread cannot use blocking APIs:
join()- Usejoin_async().awaitinsteadpark()/park_timeout()- Only works from background threadsMutex::lock()from std - Usewasm_safe_mutexinstead
Threading requires SharedArrayBuffer, which needs these HTTP headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
See Mozilla's documentation for details.
- Browser: Web Workers with shared memory
- Node.js: worker_threads module
Standard library must be rebuilt with atomics support:
# Install nightly and components
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
# Build with atomics
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' \
cargo +nightly build -Z build-std=std,panic_abort \
--target wasm32-unknown-unknownLicensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
