{"schemaVersion":1,"id":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm","title":"WASI Threads: Shared Memory Parallelism Comes to WebAssembly","description":"Shared memory WebAssembly, atomics, parallel computation, SharedArrayBuffer, exploring WASM threads through a ray tracer implementation to separate reality from hype.","language":"en","datePublished":"2025-10-31","dateModified":null,"contentFormat":"text/markdown","content":"WebAssembly\n\n# WASI Threads: Shared Memory Parallelism Comes to WebAssembly\n\n2025-10-317 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/wasi-threads-shared-memory-parallel-wasm>)\n\n## The threading model\n\nWASM threads use a shared-memory model similar to pthreads. Multiple threads (browser Web Workers or WASI thread instances) share the same linear memory. Synchronization happens through atomic operations on that shared memory.\n\nThe key primitives:\n\n1. **Shared linear memory**, Declared with the ` shared ` flag, accessible by all threads\n2. **Atomic operations**, ` i32.atomic.load `, ` i32.atomic.store `, ` i32.atomic.rmw.add `, ` i32.atomic.rmw.cmpxchg `\n3. **Wait/notify**, ` memory.atomic.wait32 ` (block until memory changes), ` memory.atomic.notify ` (wake waiting threads)\n\nHere's how you declare shared memory in WAT (WebAssembly Text format):\n\n```wat\n;; Shared memory: 256 pages (16MB) initial, 1024 pages (64MB) max\n(memory (export \"memory\") 256 1024 shared)\n```\n\nFrom C with Emscripten:\n\n```c\n// Compile with: emcc -pthread -s SHARED_MEMORY=1 -s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency\n#include <pthread.h>\n#include <stdatomic.h>\n\n#define WIDTH 800\n#define HEIGHT 600\n#define NUM_SAMPLES 64\n\n// Shared framebuffer, visible to all threads\nstatic uint32_t framebuffer[WIDTH * HEIGHT];\nstatic atomic_int next_row;\nstatic atomic_int completed_rows;\n\ntypedef struct {\n int thread_id;\n int total_threads;\n} ThreadArgs;\n\nvoid* render_thread(void* arg) {\n ThreadArgs* args = (ThreadArgs*)arg;\n\n while (1) {\n // Atomically claim the next row\n int row = atomic_fetch_add(&next_row, 1);\n if (row >= HEIGHT) break;\n\n for (int x = 0; x < WIDTH; x++) {\n // Monte Carlo ray tracing for this pixel\n float r = 0, g = 0, b = 0;\n for (int s = 0; s < NUM_SAMPLES; s++) {\n Ray ray = camera_ray(x, row, s);\n Vec3 color = trace(ray, scene, 0);\n r += color.x;\n g += color.y;\n b += color.z;\n }\n r /= NUM_SAMPLES;\n g /= NUM_SAMPLES;\n b /= NUM_SAMPLES;\n\n // Write directly to shared framebuffer\n framebuffer[row * WIDTH + x] = pack_rgba(r, g, b, 1.0f);\n }\n\n atomic_fetch_add(&completed_rows, 1);\n }\n\n return NULL;\n}\n\nint main() {\n int num_threads = 8; // Or detect from environment\n pthread_t threads[num_threads];\n ThreadArgs args[num_threads];\n\n atomic_store(&next_row, 0);\n atomic_store(&completed_rows, 0);\n\n for (int i = 0; i < num_threads; i++) {\n args[i] = (ThreadArgs){.thread_id = i, .total_threads = num_threads};\n pthread_create(&threads[i], NULL, render_thread, &args[i]);\n }\n\n for (int i = 0; i < num_threads; i++) {\n pthread_join(threads[i], NULL);\n }\n\n return 0;\n}\n```\n\nCompile and run:\n\n```bash\nemcc ray_tracer.c -o ray_tracer.js \\\n -pthread \\\n -s SHARED_MEMORY=1 \\\n -s PTHREAD_POOL_SIZE=8 \\\n -s INITIAL_MEMORY=67108864 \\\n -s MAXIMUM_MEMORY=268435456 \\\n -s ALLOW_MEMORY_GROWTH=1 \\\n -O3 -msimd128\n```\n\n## Browser integration: Workers + SharedArrayBuffer\n\nIn the browser, each WASM thread runs in a Web Worker. The shared memory is a ` SharedArrayBuffer ` that all workers reference:\n\n```javascript\n// main.js, spawns workers and shares memory\nconst memory = new WebAssembly.Memory({\n initial: 256,\n maximum: 1024,\n shared: true // This is the key flag\n});\n\nconst numThreads = navigator.hardwareConcurrency || 4;\nconst workers = [];\n\n// Load the WASM module\nconst response = await fetch('ray_tracer.wasm');\nconst wasmBytes = await response.arrayBuffer();\n\nfor (let i = 0; i < numThreads; i++) {\n const worker = new Worker('worker.js');\n worker.postMessage({\n type: 'init',\n wasmBytes,\n memory,\n threadId: i,\n totalThreads: numThreads\n });\n workers.push(worker);\n}\n\n// Read the framebuffer and render to canvas\nconst canvas = document.getElementById('canvas');\nconst ctx = canvas.getContext('2d');\nconst imageData = ctx.createImageData(800, 600);\n\nfunction renderFrame() {\n // Read directly from shared memory\n const pixels = new Uint8ClampedArray(memory.buffer, 0, 800 * 600 * 4);\n imageData.data.set(pixels);\n ctx.putImageData(imageData, 0, 0);\n requestAnimationFrame(renderFrame);\n}\n\nrenderFrame();\n```\n\n```javascript\n// worker.js\nself.onmessage = async (e) => {\n if (e.data.type === 'init') {\n const { wasmBytes, memory, threadId, totalThreads } = e.data;\n\n const imports = {\n env: {\n memory,\n __thread_id: threadId,\n __total_threads: totalThreads\n }\n };\n\n const { instance } = await WebAssembly.instantiate(wasmBytes, imports);\n instance.exports.render_thread();\n }\n};\n```\n\nThe critical requirement: your page must set security headers for ` SharedArrayBuffer ` to work:\n\n```\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\n```\n\nWithout these headers (introduced after the Spectre vulnerability), ` SharedArrayBuffer ` is disabled and your shared memory won't work. This is the most common deployment gotcha.\n\n## Performance: the real numbers\n\nBenchmarking the ray tracer across configurations reveals telling results. The scene is a Cornell box with 4 spheres, 64 samples per pixel, 800x600 resolution:\n\n```\nNative C (gcc -O3, 8 threads): 1,240ms\nWASM threads (Chrome, 8 workers): 1,890ms (1.52x native)\nWASM threads (Firefox, 8 workers): 2,100ms (1.69x native)\nWASM single-thread (Chrome): 9,450ms (7.62x native)\nJavaScript (hand-optimized, 8 workers): 14,200ms (11.45x native)\n```\n\nWASM threads achieve 52-66% of native performance, a substantial improvement over single-threaded WASM (4-5x speedup from parallelism) and dramatically faster than JavaScript. The overhead versus native comes from:\n\n1. **Bounds checking** on shared memory access (~15% overhead)\n2. **Worker creation cost** (~50ms per worker, amortized over the render)\n3. **No SIMD auto-vectorization** across thread boundaries\n\nThe scaling is near-linear up to the physical core count:\n\n```\n1 thread: 9,450ms\n2 threads: 4,780ms (1.98x speedup)\n4 threads: 2,410ms (3.92x speedup)\n8 threads: 1,890ms (5.00x speedup, 8 logical cores, 4 physical)\n```\n\nHyperthreading gives diminishing returns, as expected for compute-bound workloads.\n\n## WASI threads outside the browser\n\nThe WASI threads proposal (` wasi-threads `) brings the same model to server-side WASM runtimes. [Wasmtime](<https://wasmtime.dev/>) and [Wasmer](<https://wasmer.io/>) both support it:\n\n```bash\n# Compile with wasi-threads target\nclang --target=wasm32-wasi-threads \\\n -pthread \\\n -matomics -mbulk-memory \\\n -Wl,--import-memory,--shared-memory,--max-memory=67108864 \\\n -o ray_tracer.wasm ray_tracer.c\n\n# Run with wasmtime\nwasmtime run --wasi threads ray_tracer.wasm\n```\n\nIn Rust, the ` wasm32-wasi-threads ` target is experimental but functional:\n\n```rust\n// Cargo.toml\n[target.wasm32-wasi-threads]\nrustflags = [\"-C\", \"target-feature=+atomics,+bulk-memory\"]\n\n// src/main.rs\nuse std::sync::atomic::{AtomicI32, Ordering};\nuse std::sync::Arc;\n\nfn main() {\n let counter = Arc::new(AtomicI32::new(0));\n let mut handles = vec![];\n\n for _ in 0..8 {\n let counter = Arc::clone(&counter);\n let handle = std::thread::spawn(move || {\n for _ in 0..1000 {\n counter.fetch_add(1, Ordering::Relaxed);\n }\n });\n handles.push(handle);\n }\n\n for handle in handles {\n handle.join().unwrap();\n }\n\n println!(\"Counter: {}\", counter.load(Ordering::SeqCst));\n assert_eq!(counter.load(Ordering::SeqCst), 8000);\n}\n```\n\n```bash\ncargo build --target wasm32-wasi-threads --release\nwasmtime run --wasi threads target/wasm32-wasi-threads/release/threaded_counter.wasm\n```\n\n## Synchronization primitives\n\nThe atomic operations map directly to WASM instructions:\n\n```c\n// Mutex implementation using WASM atomics\ntypedef struct {\n _Atomic int locked;\n} WasmMutex;\n\nvoid wasm_mutex_lock(WasmMutex* m) {\n while (1) {\n int expected = 0;\n if (atomic_compare_exchange_weak(&m->locked, &expected, 1)) {\n return; // Acquired\n }\n // Wait until the lock is released\n __builtin_wasm_memory_atomic_wait32(&m->locked, 1, -1);\n }\n}\n\nvoid wasm_mutex_unlock(WasmMutex* m) {\n atomic_store(&m->locked, 0);\n // Wake one waiting thread\n __builtin_wasm_memory_atomic_notify(&m->locked, 1);\n}\n```\n\nThe ` wait32/notify ` pair is crucial. Without it, you'd use a spin-lock that burns CPU cycles. ` wait32 ` tells the runtime \"put this thread to sleep until the value at this memory address changes,\" which is essentially a futex.\n\n## Where this matters\n\nWASM threads unlock workloads that were previously impractical in the browser or in sandboxed WASM environments:\n\n- **Image/video processing**: Parallel pixel operations, codec decoding\n- **Scientific computing**: Monte Carlo simulations, matrix operations\n- **Game engines**: Physics simulation, AI pathfinding on separate threads\n- **Compression**: Parallel block compression (zstd, brotli)\n- **Machine learning inference**: Parallel matrix multiplication for small models\n\nA ray tracer serves as an effective proof of concept, but the pattern applies broadly. Any embarrassingly parallel workload, where you can divide work into independent chunks and aggregate results, benefits directly.\n\nThe remaining gaps: no thread-local storage in the WASM spec yet (workaround: manual offset into shared memory), no ` pthread_cond_t ` equivalent (workaround: build on ` wait32/notify `), and the browser's worker pool has a creation overhead that makes short-lived threads expensive (workaround: keep workers alive and dispatch work to them).\n\nWASM threads aren't a complete replacement for native threading. They're a good-enough approximation that brings real parallelism to sandboxed environments. For the workloads that benefit, and the ray tracer's 5x speedup from 8 threads proves they exist, that's enough to change what's possible.\n\n## Frequently asked questions\n\n### What are WASI threads?\n\nWASI threads is a proposal that adds thread creation and shared memory support to the WebAssembly System Interface. It allows WASM modules to spawn threads that share the same linear memory, enabling true parallel computation.\n\n### Can I use WASM threads in the browser?\n\nYes, via SharedArrayBuffer and Web Workers. Each worker loads the same WASM module with shared memory. The COOP and COEP headers must be set for SharedArrayBuffer to be available.\n\n### How does WASM thread performance compare to native?\n\nIn compute-bound workloads, WASM threads achieve 60-80% of native thread performance. The overhead comes from bounds checking on shared memory access and the browser's worker creation cost.\n\n## Tips\n\n1. **Tip:** Use ` memory.atomic.wait32 ` and ` memory.atomic.notify ` for thread synchronization instead of spin-locks. Spin-locks waste CPU cycles and thrash the cache; wait/notify yields to the scheduler.\n2. **Tip:** Always compile with ` -pthread -matomics -mbulk-memory ` when targeting WASM threads from C/C++. Missing any flag produces a binary that silently falls back to single-threaded behavior.\n3. **Tip:** Set ` Cross-Origin-Opener-Policy: same-origin ` and ` Cross-Origin-Embedder-Policy: require-corp ` headers on your HTML page. Without these, browsers disable ` SharedArrayBuffer ` and your WASM threads won't share memory.\n4. **Tip:** Profile with ` performance.measureUserAgentSpecificMemory() ` to track per-thread memory usage. Standard heap profilers don't distinguish shared vs private memory in WASM.\n\n- wasi\n- wasm\n- threads\n- shared-memory\n- atomics\n- parallel-computing\n- webassembly\n- ray-tracer\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"WASI Threads: Shared Memory Parallelism Comes to WebAssembly","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#title"},{"level":2,"text":"The threading model","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#the-threading-model"},{"level":2,"text":"Browser integration: Workers + SharedArrayBuffer","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#browser-integration-workers-sharedarraybuffer"},{"level":2,"text":"Performance: the real numbers","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#performance-the-real-numbers"},{"level":2,"text":"WASI threads outside the browser","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#wasi-threads-outside-the-browser"},{"level":2,"text":"Synchronization primitives","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#synchronization-primitives"},{"level":2,"text":"Where this matters","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#where-this-matters"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#questions"},{"level":3,"text":"What are WASI threads?","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#question-1"},{"level":3,"text":"Can I use WASM threads in the browser?","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#question-2"},{"level":3,"text":"How does WASM thread performance compare to native?","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#article-tips"}],"sections":[{"heading":"WASI Threads: Shared Memory Parallelism Comes to WebAssembly","level":1,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm","content":"WebAssembly"},{"heading":"WASI Threads: Shared Memory Parallelism Comes to WebAssembly","level":1,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#title","content":"2025-10-317 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/wasi-threads-shared-memory-parallel-wasm>)"},{"heading":"The threading model","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#the-threading-model","content":"WASM threads use a shared-memory model similar to pthreads. Multiple threads (browser Web Workers or WASI thread instances) share the same linear memory. Synchronization happens through atomic operations on that shared memory.\n\nThe key primitives:\n\n1. **Shared linear memory**, Declared with the ` shared ` flag, accessible by all threads\n2. **Atomic operations**, ` i32.atomic.load `, ` i32.atomic.store `, ` i32.atomic.rmw.add `, ` i32.atomic.rmw.cmpxchg `\n3. **Wait/notify**, ` memory.atomic.wait32 ` (block until memory changes), ` memory.atomic.notify ` (wake waiting threads)\n\nHere's how you declare shared memory in WAT (WebAssembly Text format):\n\n```wat\n;; Shared memory: 256 pages (16MB) initial, 1024 pages (64MB) max\n(memory (export \"memory\") 256 1024 shared)\n```\n\nFrom C with Emscripten:\n\n```c\n// Compile with: emcc -pthread -s SHARED_MEMORY=1 -s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency\n#include <pthread.h>\n#include <stdatomic.h>\n\n#define WIDTH 800\n#define HEIGHT 600\n#define NUM_SAMPLES 64\n\n// Shared framebuffer, visible to all threads\nstatic uint32_t framebuffer[WIDTH * HEIGHT];\nstatic atomic_int next_row;\nstatic atomic_int completed_rows;\n\ntypedef struct {\n int thread_id;\n int total_threads;\n} ThreadArgs;\n\nvoid* render_thread(void* arg) {\n ThreadArgs* args = (ThreadArgs*)arg;\n\n while (1) {\n // Atomically claim the next row\n int row = atomic_fetch_add(&next_row, 1);\n if (row >= HEIGHT) break;\n\n for (int x = 0; x < WIDTH; x++) {\n // Monte Carlo ray tracing for this pixel\n float r = 0, g = 0, b = 0;\n for (int s = 0; s < NUM_SAMPLES; s++) {\n Ray ray = camera_ray(x, row, s);\n Vec3 color = trace(ray, scene, 0);\n r += color.x;\n g += color.y;\n b += color.z;\n }\n r /= NUM_SAMPLES;\n g /= NUM_SAMPLES;\n b /= NUM_SAMPLES;\n\n // Write directly to shared framebuffer\n framebuffer[row * WIDTH + x] = pack_rgba(r, g, b, 1.0f);\n }\n\n atomic_fetch_add(&completed_rows, 1);\n }\n\n return NULL;\n}\n\nint main() {\n int num_threads = 8; // Or detect from environment\n pthread_t threads[num_threads];\n ThreadArgs args[num_threads];\n\n atomic_store(&next_row, 0);\n atomic_store(&completed_rows, 0);\n\n for (int i = 0; i < num_threads; i++) {\n args[i] = (ThreadArgs){.thread_id = i, .total_threads = num_threads};\n pthread_create(&threads[i], NULL, render_thread, &args[i]);\n }\n\n for (int i = 0; i < num_threads; i++) {\n pthread_join(threads[i], NULL);\n }\n\n return 0;\n}\n```\n\nCompile and run:\n\n```bash\nemcc ray_tracer.c -o ray_tracer.js \\\n -pthread \\\n -s SHARED_MEMORY=1 \\\n -s PTHREAD_POOL_SIZE=8 \\\n -s INITIAL_MEMORY=67108864 \\\n -s MAXIMUM_MEMORY=268435456 \\\n -s ALLOW_MEMORY_GROWTH=1 \\\n -O3 -msimd128\n```"},{"heading":"Browser integration: Workers + SharedArrayBuffer","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#browser-integration-workers-sharedarraybuffer","content":"In the browser, each WASM thread runs in a Web Worker. The shared memory is a ` SharedArrayBuffer ` that all workers reference:\n\n```javascript\n// main.js, spawns workers and shares memory\nconst memory = new WebAssembly.Memory({\n initial: 256,\n maximum: 1024,\n shared: true // This is the key flag\n});\n\nconst numThreads = navigator.hardwareConcurrency || 4;\nconst workers = [];\n\n// Load the WASM module\nconst response = await fetch('ray_tracer.wasm');\nconst wasmBytes = await response.arrayBuffer();\n\nfor (let i = 0; i < numThreads; i++) {\n const worker = new Worker('worker.js');\n worker.postMessage({\n type: 'init',\n wasmBytes,\n memory,\n threadId: i,\n totalThreads: numThreads\n });\n workers.push(worker);\n}\n\n// Read the framebuffer and render to canvas\nconst canvas = document.getElementById('canvas');\nconst ctx = canvas.getContext('2d');\nconst imageData = ctx.createImageData(800, 600);\n\nfunction renderFrame() {\n // Read directly from shared memory\n const pixels = new Uint8ClampedArray(memory.buffer, 0, 800 * 600 * 4);\n imageData.data.set(pixels);\n ctx.putImageData(imageData, 0, 0);\n requestAnimationFrame(renderFrame);\n}\n\nrenderFrame();\n```\n\n```javascript\n// worker.js\nself.onmessage = async (e) => {\n if (e.data.type === 'init') {\n const { wasmBytes, memory, threadId, totalThreads } = e.data;\n\n const imports = {\n env: {\n memory,\n __thread_id: threadId,\n __total_threads: totalThreads\n }\n };\n\n const { instance } = await WebAssembly.instantiate(wasmBytes, imports);\n instance.exports.render_thread();\n }\n};\n```\n\nThe critical requirement: your page must set security headers for ` SharedArrayBuffer ` to work:\n\n```\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\n```\n\nWithout these headers (introduced after the Spectre vulnerability), ` SharedArrayBuffer ` is disabled and your shared memory won't work. This is the most common deployment gotcha."},{"heading":"Performance: the real numbers","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#performance-the-real-numbers","content":"Benchmarking the ray tracer across configurations reveals telling results. The scene is a Cornell box with 4 spheres, 64 samples per pixel, 800x600 resolution:\n\n```\nNative C (gcc -O3, 8 threads): 1,240ms\nWASM threads (Chrome, 8 workers): 1,890ms (1.52x native)\nWASM threads (Firefox, 8 workers): 2,100ms (1.69x native)\nWASM single-thread (Chrome): 9,450ms (7.62x native)\nJavaScript (hand-optimized, 8 workers): 14,200ms (11.45x native)\n```\n\nWASM threads achieve 52-66% of native performance, a substantial improvement over single-threaded WASM (4-5x speedup from parallelism) and dramatically faster than JavaScript. The overhead versus native comes from:\n\n1. **Bounds checking** on shared memory access (~15% overhead)\n2. **Worker creation cost** (~50ms per worker, amortized over the render)\n3. **No SIMD auto-vectorization** across thread boundaries\n\nThe scaling is near-linear up to the physical core count:\n\n```\n1 thread: 9,450ms\n2 threads: 4,780ms (1.98x speedup)\n4 threads: 2,410ms (3.92x speedup)\n8 threads: 1,890ms (5.00x speedup, 8 logical cores, 4 physical)\n```\n\nHyperthreading gives diminishing returns, as expected for compute-bound workloads."},{"heading":"WASI threads outside the browser","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#wasi-threads-outside-the-browser","content":"The WASI threads proposal (` wasi-threads `) brings the same model to server-side WASM runtimes. [Wasmtime](<https://wasmtime.dev/>) and [Wasmer](<https://wasmer.io/>) both support it:\n\n```bash\n# Compile with wasi-threads target\nclang --target=wasm32-wasi-threads \\\n -pthread \\\n -matomics -mbulk-memory \\\n -Wl,--import-memory,--shared-memory,--max-memory=67108864 \\\n -o ray_tracer.wasm ray_tracer.c\n\n# Run with wasmtime\nwasmtime run --wasi threads ray_tracer.wasm\n```\n\nIn Rust, the ` wasm32-wasi-threads ` target is experimental but functional:\n\n```rust\n// Cargo.toml\n[target.wasm32-wasi-threads]\nrustflags = [\"-C\", \"target-feature=+atomics,+bulk-memory\"]\n\n// src/main.rs\nuse std::sync::atomic::{AtomicI32, Ordering};\nuse std::sync::Arc;\n\nfn main() {\n let counter = Arc::new(AtomicI32::new(0));\n let mut handles = vec![];\n\n for _ in 0..8 {\n let counter = Arc::clone(&counter);\n let handle = std::thread::spawn(move || {\n for _ in 0..1000 {\n counter.fetch_add(1, Ordering::Relaxed);\n }\n });\n handles.push(handle);\n }\n\n for handle in handles {\n handle.join().unwrap();\n }\n\n println!(\"Counter: {}\", counter.load(Ordering::SeqCst));\n assert_eq!(counter.load(Ordering::SeqCst), 8000);\n}\n```\n\n```bash\ncargo build --target wasm32-wasi-threads --release\nwasmtime run --wasi threads target/wasm32-wasi-threads/release/threaded_counter.wasm\n```"},{"heading":"Synchronization primitives","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#synchronization-primitives","content":"The atomic operations map directly to WASM instructions:\n\n```c\n// Mutex implementation using WASM atomics\ntypedef struct {\n _Atomic int locked;\n} WasmMutex;\n\nvoid wasm_mutex_lock(WasmMutex* m) {\n while (1) {\n int expected = 0;\n if (atomic_compare_exchange_weak(&m->locked, &expected, 1)) {\n return; // Acquired\n }\n // Wait until the lock is released\n __builtin_wasm_memory_atomic_wait32(&m->locked, 1, -1);\n }\n}\n\nvoid wasm_mutex_unlock(WasmMutex* m) {\n atomic_store(&m->locked, 0);\n // Wake one waiting thread\n __builtin_wasm_memory_atomic_notify(&m->locked, 1);\n}\n```\n\nThe ` wait32/notify ` pair is crucial. Without it, you'd use a spin-lock that burns CPU cycles. ` wait32 ` tells the runtime \"put this thread to sleep until the value at this memory address changes,\" which is essentially a futex."},{"heading":"Where this matters","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#where-this-matters","content":"WASM threads unlock workloads that were previously impractical in the browser or in sandboxed WASM environments:\n\n- **Image/video processing**: Parallel pixel operations, codec decoding\n- **Scientific computing**: Monte Carlo simulations, matrix operations\n- **Game engines**: Physics simulation, AI pathfinding on separate threads\n- **Compression**: Parallel block compression (zstd, brotli)\n- **Machine learning inference**: Parallel matrix multiplication for small models\n\nA ray tracer serves as an effective proof of concept, but the pattern applies broadly. Any embarrassingly parallel workload, where you can divide work into independent chunks and aggregate results, benefits directly.\n\nThe remaining gaps: no thread-local storage in the WASM spec yet (workaround: manual offset into shared memory), no ` pthread_cond_t ` equivalent (workaround: build on ` wait32/notify `), and the browser's worker pool has a creation overhead that makes short-lived threads expensive (workaround: keep workers alive and dispatch work to them).\n\nWASM threads aren't a complete replacement for native threading. They're a good-enough approximation that brings real parallelism to sandboxed environments. For the workloads that benefit, and the ray tracer's 5x speedup from 8 threads proves they exist, that's enough to change what's possible."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#questions","content":""},{"heading":"What are WASI threads?","level":3,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#question-1","content":"WASI threads is a proposal that adds thread creation and shared memory support to the WebAssembly System Interface. It allows WASM modules to spawn threads that share the same linear memory, enabling true parallel computation."},{"heading":"Can I use WASM threads in the browser?","level":3,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#question-2","content":"Yes, via SharedArrayBuffer and Web Workers. Each worker loads the same WASM module with shared memory. The COOP and COEP headers must be set for SharedArrayBuffer to be available."},{"heading":"How does WASM thread performance compare to native?","level":3,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#question-3","content":"In compute-bound workloads, WASM threads achieve 60-80% of native thread performance. The overhead comes from bounds checking on shared memory access and the browser's worker creation cost."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#article-tips","content":"1. **Tip:** Use ` memory.atomic.wait32 ` and ` memory.atomic.notify ` for thread synchronization instead of spin-locks. Spin-locks waste CPU cycles and thrash the cache; wait/notify yields to the scheduler.\n2. **Tip:** Always compile with ` -pthread -matomics -mbulk-memory ` when targeting WASM threads from C/C++. Missing any flag produces a binary that silently falls back to single-threaded behavior.\n3. **Tip:** Set ` Cross-Origin-Opener-Policy: same-origin ` and ` Cross-Origin-Embedder-Policy: require-corp ` headers on your HTML page. Without these, browsers disable ` SharedArrayBuffer ` and your WASM threads won't share memory.\n4. **Tip:** Profile with ` performance.measureUserAgentSpecificMemory() ` to track per-thread memory usage. Standard heap profilers don't distinguish shared vs private memory in WASM.\n\n- wasi\n- wasm\n- threads\n- shared-memory\n- atomics\n- parallel-computing\n- webassembly\n- ray-tracer\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/wasi-threads-shared-memory-parallel-wasm"},{"text":"Wasmtime","url":"https://wasmtime.dev/"},{"text":"Wasmer","url":"https://wasmer.io/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm"},"headline":"WASI Threads: Shared Memory Parallelism Comes to WebAssembly","description":"Shared memory WebAssembly, atomics, parallel computation, SharedArrayBuffer, exploring WASM threads through a ray tracer implementation to separate reality from hype.","datePublished":"2025-10-31","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"WebAssembly","keywords":"wasi, wasm, threads, shared-memory, atomics, parallel-computing, webassembly, ray-tracer","wordCount":1170,"timeRequired":"PT7M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/wasi-threads-shared-memory-parallel-wasm"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"WASI Threads: Shared Memory Parallelism Comes to WebAssembly","item":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What are WASI threads?","acceptedAnswer":{"@type":"Answer","text":"WASI threads is a proposal that adds thread creation and shared memory support to the WebAssembly System Interface. It allows WASM modules to spawn threads that share the same linear memory, enabling true parallel computation."}},{"@type":"Question","name":"Can I use WASM threads in the browser?","acceptedAnswer":{"@type":"Answer","text":"Yes, via SharedArrayBuffer and Web Workers. Each worker loads the same WASM module with shared memory. The COOP and COEP headers must be set for SharedArrayBuffer to be available."}},{"@type":"Question","name":"How does WASM thread performance compare to native?","acceptedAnswer":{"@type":"Answer","text":"In compute-bound workloads, WASM threads achieve 60-80% of native thread performance. The overhead comes from bounds checking on shared memory access and the browser's worker creation cost."}}]}]}],"contentHash":"8b19835ac562532cb000907c0b0d0fccf28380e42b293c7e2a38965dadaa5b98","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/wasi-threads-shared-memory-parallel-wasm","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":59409,"contentBytes":11337}}
