JS
() =>
JavaScript · Web Workers · Shared Memory · Concurrency
Atomics und SharedArrayBuffer
Echte Parallelverarbeitung in JavaScript

JavaScript ist von Natur aus single-threaded – aber mit SharedArrayBuffer und Atomics gibt es echten Shared Memory zwischen Threads. Damit lassen sich Szenarien lösen, die mit postMessage nicht effizient zu realisieren waren: schnelle Bildverarbeitung, Parser, WASM-Integration und Lock-freie Datenstrukturen.

15 Min. Lesezeit SharedArrayBuffer · Atomics · Web Workers · COOP/COEP Browser · Node.js · Deno

1. Warum Shared Memory in JavaScript?

JavaScript wurde für single-threaded Ausführung entworfen. Die Event-Loop verarbeitet einen Callback nach dem anderen, ohne echte Parallelität. Web Workers haben dieses Modell erweitert: Sie laufen in separaten Threads mit eigenem Heap. Die Kommunikation zwischen Workers und dem Hauptthread erfolgt über postMessage, das Daten serialisiert, überträgt und auf der Empfängerseite deserialisiert. Für kleine Datenmenge ist das ausreichend – für große Binärdaten, Bilder oder kontinuierliche Datenströme ist die Serialisierung der Engpass.

SharedArrayBuffer löst dieses Problem fundamental: Statt Daten zwischen Threads zu kopieren, teilen sich alle Threads denselben Speicherbereich. Ein Web Worker kann direkt in denselben Buffer schreiben, aus dem der Hauptthread liest – ohne Kopieren, ohne Serialisierung, ohne Overhead. Das ermöglicht Szenarien, die vorher nicht in JavaScript umsetzbar waren: echte Lock-freie Datenstrukturen, effiziente Bildpipelines, Parser die große Dateien ohne Kopierschritte verarbeiten und WASM-Module, die mit JavaScript-Code Speicher teilen.

2. SharedArrayBuffer: Speicher zwischen Threads teilen

Ein SharedArrayBuffer ist ein Rohspeicher-Objekt, ähnlich wie ArrayBuffer, aber mit dem entscheidenden Unterschied: Er kann an mehrere Worker übergeben werden, ohne kopiert zu werden. Wo ein normaler ArrayBuffer bei der Übertragung via postMessage transferiert wird (der Sender verliert den Zugriff), kann ein SharedArrayBuffer von beliebig vielen Threads gleichzeitig gelesen und geschrieben werden. Der Speicher bleibt bestehen, bis alle Referenzen auf ihn aufgelöst sind.

Auf den SharedArrayBuffer wird über typisierte Arrays zugegriffen: Int32Array, Uint8Array, Float64Array und andere. Diese Views zeigen auf denselben Speicherbereich und ermöglichen effizient typisierten Zugriff. Schreibt ein Worker in den Buffer via Int32Array, sieht der andere Worker diese Änderung sofort – ohne Message-Passing, ohne Verzögerung. Das ist echte Shared-Memory-Kommunikation, wie sie von C++ und Java-Programmierern seit Jahrzehnten genutzt wird, nun auch in JavaScript verfügbar.


// Main thread: create shared buffer and send to worker
const sharedBuffer = new SharedArrayBuffer(4096); // 4 KB shared memory
const sharedArray = new Int32Array(sharedBuffer);

// Initialize with values
sharedArray[0] = 0; // status flag
sharedArray[1] = 0; // counter

const worker = new Worker('worker.js');

// Transfer reference — no copy, both share the same memory
worker.postMessage({ sharedBuffer });

// Read from main thread — sees worker writes immediately
setTimeout(() => {
  console.log('Counter from worker:', sharedArray[1]);
}, 1000);

// --- worker.js ---
self.onmessage = ({ data: { sharedBuffer } }) => {
  const sharedArray = new Int32Array(sharedBuffer);

  // Write directly to shared memory
  for (let i = 0; i < 1000; i++) {
    sharedArray[1]++; // increment shared counter
  }

  self.postMessage('done');
};

3. Sicherheitsanforderungen: COOP und COEP Header

Nach dem Spectre-Angriff 2018 wurde SharedArrayBuffer in Browsern temporär deaktiviert. Der Grund: Shared Memory mit hoher zeitlicher Auflösung ermöglicht Timing-Angriffe, die CPU-Cache-Seitenkanäle ausnutzen. Die Lösung war nicht die Abschaffung von SharedArrayBuffer, sondern die Einführung von Site-Isolation-Anforderungen. Seit 2020 ist SharedArrayBuffer wieder verfügbar – aber nur in sogenannten "cross-origin isolated" Kontexten.

Konkret bedeutet das: Der Server muss zwei HTTP-Header senden. Cross-Origin-Opener-Policy: same-origin (COOP) verhindert, dass andere Seiten das Browserfenster referenzieren können. Cross-Origin-Embedder-Policy: require-corp (COEP) stellt sicher, dass alle Ressourcen auf der Seite explizit den Cross-Origin-Zugriff erlauben. Beide Header zusammen aktivieren den "cross-origin isolated"-Modus, der für SharedArrayBuffer und hohe Zeitauflösung in performance.now() erforderlich ist. Wer diese Header nicht setzt, bekommt eine SecurityError-Exception beim Erstellen eines SharedArrayBuffer.

4. Atomics: atomare Operationen ohne Race Conditions

Wenn mehrere Threads gleichzeitig in denselben Speicherbereich schreiben, entsteht ohne Synchronisation eine Race Condition. Das klassische Problem: Thread A liest einen Wert (z.B. 5), Thread B liest denselben Wert (5), Thread A schreibt 6 zurück, Thread B schreibt ebenfalls 6 zurück – obwohl beide einmal inkrementiert haben, steht im Speicher nur 6, nicht 7. Das nennt man ein "lost update". Das Atomics-Objekt in JavaScript löst dieses Problem durch atomare Operationen: Lesen-Modifizieren-Schreiben in einem einzigen, unteilbaren Schritt.

Atomics.add(array, index, value) inkrementiert den Wert an einer Position atomisch – kein anderer Thread kann dazwischenkommen. Atomics.load(array, index) liest einen Wert mit garantierter Sichtbarkeit der neuesten Schreiboperation anderer Threads. Atomics.store(array, index, value) schreibt einen Wert, der sofort für alle anderen Threads sichtbar ist. Atomics arbeitet ausschließlich mit Int8Array, Int16Array, Int32Array, BigInt64Array und deren unsigned Varianten – alle über einem SharedArrayBuffer.


// Atomic operations — safe concurrent access without race conditions
const sab = new SharedArrayBuffer(16);
const int32 = new Int32Array(sab);

// Atomic increment — returns OLD value, no lost updates
const oldValue = Atomics.add(int32, 0, 1);
console.log('Was:', oldValue, 'Now:', int32[0]);

// Atomic compare-and-exchange: only writes if current value matches expected
// Returns the value BEFORE the exchange (whether it succeeded or not)
const wasExpected = Atomics.compareExchange(
  int32, // typed array
  0,     // index
  5,     // expected value — only swap if current === 5
  10,    // replacement value
);

if (wasExpected === 5) {
  console.log('Exchange succeeded — now 10');
} else {
  console.log('Exchange failed — was', wasExpected, 'not 5');
}

// Atomic AND, OR, XOR for bit manipulation
Atomics.or(int32, 1, 0b00001000); // set bit 3 atomically
Atomics.and(int32, 1, 0b11110111); // clear bit 3 atomically

// Fence: ensure all prior stores are visible before continuing
Atomics.store(int32, 2, 1); // write sentinel value
// Any Atomics.load() after this on another thread will see the store above

5. Atomics.wait() und Atomics.notify() – Thread-Synchronisation

Atomics.wait() und Atomics.notify() implementieren das klassische Condvar-Muster (Condition Variable) aus der systemnahen Programmierung. Ein Thread ruft Atomics.wait(array, index, expectedValue) auf und blockiert, bis der Wert an der angegebenen Position nicht mehr expectedValue ist – oder bis ein Timeout abläuft. Ein anderer Thread ändert den Wert und ruft Atomics.notify(array, index, count) auf, um wartende Threads aufzuwecken. Das ist die Grundlage für Mutexe, Semaphore und Producer-Consumer-Queues in JavaScript.

Ein wichtiger Unterschied: Atomics.wait() kann nur in Worker-Threads aufgerufen werden, nicht im Hauptthread des Browsers (der darf nicht blockieren). Im Hauptthread gibt es seit 2024 Atomics.waitAsync(), das einen Promise zurückgibt statt zu blockieren. In Node.js ist Atomics.wait() im Hauptthread erlaubt, weil Node.js für I/O-intensive Workloads anders konzipiert ist. Atomics.notify() hingegen kann von überall aufgerufen werden.

6. Mutex-Implementierung mit Atomics.compareExchange()

Ein Mutex (Mutual Exclusion Lock) ist die grundlegendste Synchronisationsprimitive für geteilten Speicher. Mit Atomics.compareExchange() lässt sich ein Spinlock implementieren: Der Lock-Wert ist 0 (unlocked) oder 1 (locked). Ein Thread versucht, mit compareExchange von 0 auf 1 zu wechseln. Wenn der Rückgabewert 0 ist, hat er erfolgreich gelockt. Wenn nicht, war der Mutex bereits gesperrt, und der Thread muss warten. Mit Atomics.wait() kann er effizienter warten als mit einem Busy-Wait-Loop.

Der kritische Abschnitt (Critical Section) zwischen Lock und Unlock wird von maximal einem Thread gleichzeitig ausgeführt – alle anderen warten. Beim Entsperren setzt der Thread den Wert zurück auf 0 und ruft Atomics.notify() auf, um wartende Threads zu wecken. Dieses Pattern ist die Grundlage für alle höheren Synchronisationsprimitiven: Semaphore, Read-Write-Locks und Condition Variables. In der Praxis sind solche Strukturen die Basis für performante Parser und Encoder, die große Datenmengen in Chunks parallelisiert verarbeiten.


// Mutex implementation using Atomics.compareExchange()
// Index 0 in the Int32Array is the lock: 0=unlocked, 1=locked
class SharedMutex {
  constructor(sab, byteOffset = 0) {
    this.lock = new Int32Array(sab, byteOffset, 1);
  }

  acquire() {
    // Spin until lock is acquired
    while (true) {
      // Try to change 0 → 1; returns old value
      const prev = Atomics.compareExchange(this.lock, 0, 0, 1);
      if (prev === 0) return; // acquired successfully

      // Lock was taken — wait until notified (efficient block)
      Atomics.wait(this.lock, 0, 1);
    }
  }

  release() {
    Atomics.store(this.lock, 0, 0); // release lock
    Atomics.notify(this.lock, 0, 1); // wake one waiting thread
  }
}

// Usage in worker:
self.onmessage = ({ data: { sab } }) => {
  const mutex = new SharedMutex(sab, 0);
  const counter = new Int32Array(sab, 4, 1); // counter at offset 4

  for (let i = 0; i < 10000; i++) {
    mutex.acquire();
    counter[0]++; // critical section — safe with mutex
    mutex.release();
  }
};

7. Atomics und WebAssembly: WASM-Integration

Einer der wichtigsten Anwendungsfälle von SharedArrayBuffer und Atomics ist die Integration mit WebAssembly. WASM-Module, die aus C++, Rust oder Go kompiliert wurden, erwarten oft Shared Memory – genau das, was SharedArrayBuffer bietet. Emscripten-kompilierter C++-Code, der POSIX-Threads verwendet, nutzt intern SharedArrayBuffer für den Thread-Stack und Atomics für Pthread-Synchronisation. Ohne diese APIs wäre Threading in WASM im Browser nicht möglich.

Für JavaScript-Entwickler bedeutet das: Wenn man ein rechenintensives WASM-Modul (z.B. einen Bildfilter, Codec oder Physik-Simulator) in einem Worker laufen lässt und Ergebnisse effizient zurückbekommen will, ist SharedArrayBuffer der richtige Kanal. Statt das gesamte Ergebnis zu serialisieren und via postMessage zu übertragen, schreibt das WASM-Modul direkt in den geteilten Buffer, und der Hauptthread liest die Daten sofort. Das kann bei großen Puffern (z.B. einem Full-HD-Videobild: 8 MB) mehrere Millisekunden Latenz einsparen.

8. Praktische Anwendungsfälle im Frontend

Die drei häufigsten Anwendungsfälle für SharedArrayBuffer und Atomics im Frontend sind Bildverarbeitung, Audio-Verarbeitung und strukturierte Parallelarbeit. Bei der Bildverarbeitung arbeiten mehrere Worker auf verschiedenen Teilen eines Bildes gleichzeitig – z.B. teilt man ein 4K-Bild in vier horizontale Streifen, jeder Worker verarbeitet einen Streifen im selben SharedArrayBuffer, und das Ergebnis liegt ohne Kopierschritt im Hauptthread bereit. Das ermöglicht Echtzeitfilter, die ohne Shared Memory zu langsam wären.

Für Audio bietet die Web Audio API seit neuem den AudioWorkletProcessor, der in einem eigenen Thread läuft. Mit SharedArrayBuffer kann ein Analyse-Worker permanent Audio-Samples aus dem Worklet-Thread lesen, ohne dass jedes Sample via postMessage übertragen werden muss. Das eliminiert die Jitter-Probleme, die bei Message-basiertem Audio-Streaming entstehen. Als dritter Anwendungsfall: JSON-Parser und Kompressionsalgorithmen, die large Inputs parallel in Chunks verarbeiten – jeder Worker nimmt sich einen Chunk aus einem geteilten Input-Buffer und schreibt das Ergebnis in einen geteilten Output-Buffer.

9. Atomics vs. postMessage: Vergleich

Die Wahl zwischen Atomics/SharedArrayBuffer und postMessage hängt vom Datenvolumen und der Kommunikationsfrequenz ab. Für seltene, kleine Nachrichten ist postMessage einfacher und ausreichend. Für hochfrequente Kommunikation oder große Datenmengen ist Shared Memory klar überlegen. Die Entscheidungsgrundlage liegt in der Messung: postMessage mit einem 1 MB ArrayBuffer kostet typischerweise 1-5 ms für die Serialisierung und den Transfer; der gleiche Zugriff via SharedArrayBuffer kostet Nanosekunden.

Kriterium postMessage SharedArrayBuffer + Atomics Empfehlung
Serialisierungsaufwand O(n) – strukturiertes Klonen Keiner – Direktzugriff Shared Memory bei >10 KB
Synchronisation Automatisch (Kopie) Manuell (Atomics) postMessage für einfache Fälle
Setup-Komplexität Gering Hoch (COOP/COEP Header) Shared Memory nur wenn nötig
Latenz bei 1 MB 1–5 ms <1 µs Shared Memory bei großen Daten
Debugging Einfach Schwer (Race Conditions) Mutex-Abstraktion verwenden

Die Grundregel: SharedArrayBuffer und Atomics sind Werkzeuge für Hochleistungsszenarien, nicht für allgemeine Worker-Kommunikation. Sie bringen echte Komplexität mit – Race Conditions, Deadlock-Risiken, COOP/COEP-Anforderungen – und sollten nur eingesetzt werden, wenn postMessage messbar zum Bottleneck wird. In der Praxis ist das selten der Fall, aber wenn es der Fall ist, gibt es keine gleichwertige Alternative im Browser-Ökosystem.

Mironsoft

JavaScript-Performance, Web Workers und WebAssembly-Integration

Hochleistungs-JavaScript mit Shared Memory implementieren?

Wir helfen bei der Konzeption und Implementierung von parallelen JavaScript-Architekturen mit SharedArrayBuffer, Atomics und WebAssembly-Integration.

Performance-Audit

Analyse ob SharedArrayBuffer ein echter Bottleneck-Löser ist oder postMessage ausreicht

Architektur

Worker-Pool-Design, Mutex-Abstraktionen und sichere Shared-Memory-Protokolle

WASM-Integration

WebAssembly mit SharedArrayBuffer verbinden und Threading für WASM-Module aktivieren

10. Zusammenfassung

SharedArrayBuffer ermöglicht echtes Shared Memory zwischen JavaScript-Threads – ohne Serialisierungsaufwand, ohne postMessage-Overhead. Atomics stellt die Synchronisationsprimitiven bereit, die für sicheren concurrent Zugriff auf diesen geteilten Speicher notwendig sind: atomare Lese-/Schreiboperationen, Compare-and-Exchange für Lock-freie Algorithmen, und wait/notify für effizientes Warten. Zusammen ermöglichen sie Parallelverarbeitungsszenarien, die vorher im Browser nicht umsetzbar waren.

Die Eintrittshürde ist bewusst hoch: COOP/COEP-Header müssen konfiguriert sein, Race Conditions müssen verstanden und vermieden werden, und die Debugging-Erfahrung ist komplexer als bei postMessage. Atomics und SharedArrayBuffer sind kein Ersatz für postMessage in normalen Worker-Szenarien – sie sind die richtige Wahl für Hochleistungsszenarien mit großen Datenmengen, WASM-Integration oder rechenintensive Bildverarbeitung. Wer diese Werkzeuge beherrscht, hat Zugang zu einer Klasse von Optimierungen, die im JavaScript-Ökosystem einzigartig ist.

Atomics und SharedArrayBuffer — Das Wichtigste auf einen Blick

SharedArrayBuffer

Geteilter Rohspeicher zwischen Threads ohne Kopieraufwand. Anforderung: COOP + COEP HTTP-Header für cross-origin isolation.

Atomics-Operationen

add, sub, and, or, xor, load, store als atomare Operationen. compareExchange für Lock-freie Algorithmen und Mutex-Implementierungen.

Thread-Synchronisation

Atomics.wait() blockiert Worker-Threads effizient. Atomics.notify() weckt wartende Threads. waitAsync() für den nicht-blockierenden Hauptthread.

Wann einsetzen

Nur wenn postMessage messbar zum Bottleneck wird. Bildverarbeitung, Audio-Worklets, WASM-Integration und große Binärdaten sind die richtigen Szenarien.

11. FAQ: Atomics und SharedArrayBuffer

1Was ist ein SharedArrayBuffer?
Rohspeicher-Objekt, das zwischen Web Workers und Hauptthread geteilt wird – ohne Kopieraufwand. Alle Threads sehen Änderungen sofort. Echtes Shared Memory in JavaScript.
2Warum braucht man COOP und COEP?
Spectre-Schutz seit 2020. Ohne Cross-Origin-Opener-Policy: same-origin und Cross-Origin-Embedder-Policy: require-corp gibt es einen SecurityError beim Erstellen des SharedArrayBuffer.
3Was macht Atomics.compareExchange()?
Liest den aktuellen Wert, vergleicht mit expected, schreibt replacement nur wenn gleich – alles atomar. Gibt Wert VOR der Operation zurück. Grundlage für Mutex-Implementierungen.
4Atomics.wait() im Hauptthread?
Nicht in Browsern – blockiert den Thread. Stattdessen Atomics.waitAsync() nutzen, das einen Promise zurückgibt. In Node.js ist Atomics.wait() im Hauptthread erlaubt.
5SharedArrayBuffer vs. transferierbarer ArrayBuffer?
Transfer: Sender verliert Zugriff. SharedArrayBuffer: Alle Threads zeigen auf denselben Speicher gleichzeitig. Kein Transfer, keine Kopie, keine Eigentumsübertragung.
6Was ist eine Race Condition?
Zwei Threads lesen und schreiben denselben Speicher ohne Synchronisation. Das Ergebnis ist nicht deterministisch. Atomics und Mutexe verhindern Race Conditions.
7Wann SharedArrayBuffer statt postMessage?
Bei großen Daten (>10 KB), hoher Frequenz (Audio, Video) oder WASM-Threading. Für normale Worker-Koordination ist postMessage einfacher und ausreichend.
8Was macht Atomics.notify()?
Weckt bis zu N Threads auf, die mit Atomics.wait() warten. Gegenstück zu wait(). Implementiert das Condvar-Muster für Thread-Koordination.
9SharedArrayBuffer in Node.js?
Vollständig unterstützt. Worker Threads in Node.js können SharedArrayBuffer teilen. Atomics.wait() ist im Hauptthread erlaubt. Kein COOP/COEP nötig.
10Race Conditions debuggen?
Mutexe um kritische Abschnitte legen und prüfen ob Problem verschwindet. Logging mit Atomics.load() statt direktem Array-Zugriff. Code auf minimale kritische Abschnitte reduzieren.