True multi-threaded algorithms in JavaScript (WebAssembly threads, parallel compute) depend on shared memory.
Workers normally communicate with postMessage, which copies data via structured clone (or transfers ownership of an ArrayBuffer). A SharedArrayBuffer is different: sending it to a worker shares the same memory, so several agents read and write the same bytes concurrently. You access it through typed arrays such as Int32Array.
Because of Spectre-style timing attacks, browsers only enable SharedArrayBuffer on cross-origin isolated pages: serve the document with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (or credentialless), and check crossOriginIsolated. Node.js has no such restriction.
Ordinary reads and writes to shared memory are data races waiting to happen. counter[0]++ is a read, an add and a write; two threads interleaving those steps lose updates. The Atomics object provides indivisible operations:
Atomics.load/Atomics.store— reads and writes with ordering guarantees.Atomics.add,sub,and,or,xor,exchange— read-modify-write, returning the old value.Atomics.compareExchange— the building block for locks and lock-free structures.
For coordination, Atomics.wait(int32, index, expected) blocks the thread until another thread calls Atomics.notify(int32, index, count). Blocking is not allowed on the browser's main thread (it would freeze the page); there use Atomics.waitAsync (ES2024), which returns a promise.
Atomic operations are sequentially consistent: all agents observe them in a single total order, and they order the plain accesses around them. Racy non-atomic accesses are still well-defined enough not to crash, but their results can be stale or surprising.
Shared memory is low-level: you're manually managing layout in a byte array. Prefer message passing unless profiling proves you need it.
Example
// counter.mjs — run with: node counter.mjs
import { Worker, isMainThread, workerData } from "node:worker_threads";
if (isMainThread) {
const shared = new SharedArrayBuffer(8); // two Int32 slots
const view = new Int32Array(shared);
const workers = Array.from({ length: 4 }, () =>
new Worker(new URL(import.meta.url), { workerData: shared }));
await Promise.all(workers.map(w => new Promise(r => w.on("exit", r))));
console.log("atomic:", Atomics.load(view, 0)); // atomic: 400000 (always)
console.log("racy: ", view[1]); // racy: often less than 400000
} else {
const view = new Int32Array(workerData); // same memory, not a copy
for (let i = 0; i < 100_000; i++) {
Atomics.add(view, 0, 1); // indivisible read-modify-write
view[1]++; // data race: updates can be lost
}
}Remember
- SharedArrayBuffer shares memory between agents instead of copying it
- Browsers require cross-origin isolation (COOP + COEP) to enable it
- Use Atomics for read-modify-write and wait/notify; never block the main thread
Watch out for
- Incrementing a shared counter with
view[i]++from several workers.