wasm_safe_thread

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.
Synchronization primitives
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
Sync example
use Mutex;
let data = new;
*data.lock_sync += 1;
assert_eq!;
Channel example
use channel;
let = channel;
tx.send_sync.unwrap;
assert_eq!;
Threading primitives
In addition to synchronization primitives, this crate provides a std::thread-like API:
spawn(), Builder, JoinHandle, park(), Thread::unpark(), thread locals, and spawn hooks.
Comparison with wasm_thread
wasm_thread is a popular crate that aims to closely replicate std::thread on wasm targets. This section compares design goals and practical tradeoffs.
Design goals
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 comparison
| 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) |
Shared capabilities
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()
Behavioral differences to know
- 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).
Implementation differences (for maintainers)
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
When to use which
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
Usage
Replace use std::thread with use wasm_safe_thread as thread:
# if cfg! //join() not reliable here
use wasm_safe_thread as thread;
// Spawn a thread
let handle = spawn;
// 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!;
API
Thread spawning
use ;
// Simple spawn
let handle = spawn;
// Convenience function for named threads
let handle = spawn_named.unwrap;
// Builder pattern for more options
let handle = new
.name
.spawn
.unwrap;
Joining threads
# if cfg! //join() not reliable here
use spawn;
// Synchronous join (works on native and some browser context - but not reliably!)
let handle = spawn;
let result = handle.join.unwrap;
assert_eq!;
// Non-blocking check
let handle = spawn;
if handle.is_finished
# drop;
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;
Thread operations
use ;
use Duration;
// Get current thread
let thread = current;
println!;
// Sleep
sleep;
// Yield to scheduler
yield_now;
Park/unpark works from background threads:
# if cfg! //join not reliable on wasm
use ;
use Duration;
let handle = spawn;
handle.thread.unpark; // Wake parked thread
handle.join.unwrap; // join() is not reliable on wasm and should be avoided
Event loop integration
#
#
#
# // JsFuture is !Send, tested separately via wasm_bindgen_test
Thread local storage
use thread_local;
use RefCell;
thread_local!
COUNTER.with;
Spawn hooks
Register callbacks that run when any thread starts:
use ;
// Register a hook
register_spawn_hook;
// Hooks run in registration order, before the thread's main function
// Remove specific hook
remove_spawn_hook;
// Clear all hooks
clear_spawn_hooks;
Async task tracking (WASM)
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 ;
task_begin;
spawn_local;
These functions are no-ops on native platforms, so you can use them unconditionally in cross-platform code.
WASM Limitations
Main thread restrictions
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
SharedArrayBuffer requirements
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.
Environment support
- Browser: Web Workers with shared memory
- Node.js: worker_threads module
Building for WASM
Standard library must be rebuilt with atomics support:
# Install nightly and components
# Build with atomics
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' \
License
Licensed 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.