Atomics.waitAsync(): nicht-blockierende Synchronisation zwischen Web Workern
AI generated
JS
() =>
JavaScript · Web Worker · Nebenlaeufigkeit
Atomics.waitAsync()
Web Worker ueber SharedArrayBuffer synchronisieren, ohne den Thread einzufrieren

Mehrere Web Worker, die sich denselben SharedArrayBuffer teilen, brauchen eine Art, aufeinander zu warten, ohne staendig in einer Schleife zu pollen. Atomics.wait() bietet genau das, blockiert dabei aber den kompletten Thread und ist deshalb im Main Thread verboten. Atomics.waitAsync() loest dasselbe Problem non-blockierend und funktioniert dadurch auch dort, wo Blockieren keine Option ist.

14 Min. Lesezeit SharedArrayBuffer · Atomics.notify Wait-Async statt Busy-Waiting

1. Das Koordinationsproblem zwischen mehreren Web Workern

Sobald mehrere Web Worker denselben Speicherbereich lesen und schreiben, entsteht dasselbe Grundproblem wie bei jeder klassischen Multithreading-Umgebung: Ohne Koordination kann ein Worker einen Wert lesen, waehrend ein anderer ihn gerade mitten in der Aktualisierung hat, was zu inkonsistenten oder unbrauchbaren Zwischenzustaenden fuehrt. Reine postMessage-Kommunikation loest das nur bedingt, weil jede Nachricht kopiert oder zumindest serialisiert werden muss und damit fuer feingranulare, haeufige Synchronisation zu langsam ist.

JavaScript bietet dafuer mit SharedArrayBuffer und den Atomics-Operationen echte, speichergestuetzte Synchronisationsprimitiven, wie man sie aus Sprachen mit direktem Thread-Zugriff kennt. Damit lassen sich Mutexe, Semaphoren oder einfache Signalisierungs-Flags bauen, die mehrere Worker koordinieren, ohne bei jeder Aktualisierung eine komplette Nachricht ueber den Message-Kanal schicken zu muessen.

2. Grundlage: SharedArrayBuffer und Cross-Origin-Isolation

Ein SharedArrayBuffer verhaelt sich aehnlich wie ein gewoehnlicher ArrayBuffer, wird aber beim Uebergeben an einen Worker per postMessage nicht kopiert, sondern tatsaechlich geteilt: Beide Seiten greifen auf denselben physischen Speicherbereich zu. Ueblicherweise wird ein Int32Array als typisierte Sicht auf den SharedArrayBuffer gelegt, weil die Atomics-Operationen auf Ganzzahl-Arrays arbeiten.

Aus Sicherheitsgruenden, insbesondere wegen Seitenkanalangriffen wie Spectre, verlangt der Browser fuer SharedArrayBuffer eine Cross-Origin-Isolation der Seite: Die HTTP-Header Cross-Origin-Opener-Policy und Cross-Origin-Embedder-Policy muessen gesetzt sein. Fehlen diese Header, ist der SharedArrayBuffer-Konstruktor schlicht nicht verfuegbar, was in der Praxis der haeufigste Stolperstein beim ersten Einsatz ist.

3. Atomics.wait(): blockierendes Warten, nur in Workern erlaubt

Atomics.wait(typedArray, index, value, timeout) prueft, ob der Wert an der angegebenen Position noch dem erwarteten value entspricht, und blockiert andernfalls den aufrufenden Thread synchron, bis sich der Wert aendert oder das Timeout ablaeuft. Diese Blockade ist eine echte Thread-Pause auf Betriebssystemebene, waehrend der keinerlei anderer Code auf demselben Thread ausgefuehrt wird, auch keine Events oder Timer.

Genau deshalb wirft der Aufruf von Atomics.wait() auf dem Main Thread einen TypeError, der Browser verweigert die Ausfuehrung von vornherein. Wuerde der Main Thread blockieren, fror die gesamte Seite ein, kein Scrollen, kein Klicken, keine Animation waere mehr moeglich, bis das Warten beendet ist. In dedizierten Workern ohne UI-Verantwortung ist dieses blockierende Verhalten dagegen unproblematisch und teils sogar erwuenscht.


// Nur in einem Worker erlaubt, blockiert den Worker-Thread
const status = Atomics.wait(sharedInt32, 0, 0, 5000);
// status ist 'ok', 'not-equal' oder 'timed-out'
if (status === 'ok') {
  verarbeiteFreigegebenenBereich();
}

4. Atomics.waitAsync(): dieselbe Wartelogik, ohne den Thread zu blockieren

Atomics.waitAsync(typedArray, index, value, timeout) hat dieselbe Signatur wie Atomics.wait(), verhaelt sich aber grundlegend anders: Statt den Thread anzuhalten, gibt die Methode sofort ein Objekt mit den Eigenschaften async und value zurueck. Ist async gleich false, war der Wert bereits abweichend oder das Ergebnis stand sofort fest, value enthaelt dann direkt das Ergebnis-String wie bei Atomics.wait().

Ist async gleich true, enthaelt value stattdessen ein Promise, das erst aufgeloest wird, wenn sich der Wert tatsaechlich aendert, das Timeout ablaeuft oder Atomics.notify() aufgerufen wird. Waehrend dieses Promise aussteht, laeuft der Thread ganz normal weiter, verarbeitet Events, fuehrt andere Tasks aus und friert an keiner Stelle ein, was der zentrale Unterschied zu Atomics.wait() ist.


const ergebnis = Atomics.waitAsync(sharedInt32, 0, 0, 5000);

if (ergebnis.async) {
  ergebnis.value.then((status) => {
    console.log('Aufgeweckt mit Status:', status); // 'ok' oder 'timed-out'
  });
} else {
  console.log('Sofortiges Ergebnis:', ergebnis.value); // z. B. 'not-equal'
}

5. Atomics.notify(): wartende Threads gezielt aufwecken

Atomics.notify(typedArray, index, count) weckt bis zu count wartende Threads auf, die an der angegebenen Speicherposition auf Atomics.wait() oder Atomics.waitAsync() warten. Ein Aufruf mit count gleich 1 weckt genau einen wartenden Thread, praktisch fuer Mutex-Szenarien mit exklusivem Zugriff, waehrend Infinity alle wartenden Threads gleichzeitig aufweckt, etwa fuer ein Broadcast-Signal an alle Worker eines Pools.

Wichtig ist, den geteilten Wert vor dem Aufruf von notify tatsaechlich zu aendern, meist per Atomics.store(), weil notify selbst keinen Wert setzt, sondern lediglich wartende Threads informiert, dass sich am Speicher etwas getan haben koennte. Die aufgeweckten Threads pruefen den Wert danach selbst erneut, was gelegentliche unnoetige Aufweckungen ohne tatsaechliche Zustandsaenderung einschliesst.


// Wert setzen und genau einen wartenden Thread aufwecken
Atomics.store(sharedInt32, 0, 1);
Atomics.notify(sharedInt32, 0, 1);

6. Praxisbeispiel: ein einfacher asynchroner Mutex ueber mehrere Worker

Ein minimaler Mutex laesst sich mit einem einzelnen Int32Array-Slot realisieren: Der Wert 0 bedeutet frei, der Wert 1 bedeutet gesperrt. Ein Worker, der die Sperre erwerben will, versucht per Atomics.compareExchange() atomar von 0 auf 1 zu wechseln. Gelingt das, besitzt er die Sperre, schlaegt es fehl, wartet er per Atomics.waitAsync() auf die naechste Freigabe, statt in einer Busy-Waiting-Schleife CPU-Zeit zu verschwenden.

Beim Freigeben setzt der Worker den Wert per Atomics.store() zurueck auf 0 und ruft anschliessend Atomics.notify() auf, um genau einen wartenden Konkurrenten aufzuwecken. Dieses Muster ist deutlich effizienter als wiederholtes Pollen per setInterval, weil kein Worker CPU-Zyklen verbraucht, waehrend er tatsaechlich nichts zu tun hat.


async function sperreErwerben(sharedInt32) {
  while (Atomics.compareExchange(sharedInt32, 0, 0, 1) !== 0) {
    const ergebnis = Atomics.waitAsync(sharedInt32, 0, 1);
    if (ergebnis.async) await ergebnis.value;
  }
}

function sperreFreigeben(sharedInt32) {
  Atomics.store(sharedInt32, 0, 0);
  Atomics.notify(sharedInt32, 0, 1);
}

7. Main Thread und Worker: warum waitAsync ueberall funktioniert

Waehrend Atomics.wait() ausschliesslich in dedizierten Workern nutzbar ist, funktioniert Atomics.waitAsync() ausdruecklich auch auf dem Main Thread, eben weil es niemals blockiert. Das erlaubt Szenarien, in denen der Main Thread selbst auf einen Zustand wartet, den ein Worker-Pool ueber SharedArrayBuffer meldet, etwa den Abschluss einer parallelen Berechnung, ohne dabei die Reaktionsfaehigkeit der Seite zu gefaehrden.

Das macht waitAsync besonders wertvoll fuer Architekturen, in denen ein zentraler Koordinator im Main Thread den Fortschritt mehrerer Worker ueberwacht: Statt fuer jede Fortschrittsmeldung eine eigene postMessage-Nachricht zu verschicken, koennen die Worker einfach einen gemeinsamen Zaehler per Atomics.add() erhoehen, und der Main Thread wacht per waitAsync ueber Grenzwerte auf, ganz ohne staendiges Polling.

8. Performance: warum Spin-Locks und Polling teurer sind

Eine naive Alternative zu waitAsync waere ein Spin-Lock, bei dem ein Worker in einer engen Schleife wiederholt den Wert prueft, bis er sich aendert. Das verbraucht durchgehend CPU-Zeit, selbst waehrend nichts Sinnvolles passiert, und konkurriert dabei aktiv mit anderen Threads um Rechenzeit, was auf Geraeten mit wenigen CPU-Kernen die Gesamtleistung der Seite spuerbar verschlechtert.

Atomics.waitAsync() dagegen registriert den wartenden Thread beim Betriebssystem-Scheduler und gibt die Kontrolle vollstaendig zurueck, bis eine tatsaechliche Zustandsaenderung eintritt. Der Browser muss dabei kein Promise-Polling in Millisekunden-Intervallen durchfuehren, sondern wird von der zugrunde liegenden Thread-Synchronisationsprimitive direkt benachrichtigt, was den Ressourcenverbrauch nahezu auf null reduziert, solange kein Ereignis eintritt.

9. Grenzen, Browser-Support und Vergleich der Mechanismen

Atomics.waitAsync() setzt wie SharedArrayBuffer selbst eine cross-origin-isolierte Umgebung voraus und ist in modernen Chromium- und Firefox-Versionen verfuegbar, in aelteren Browsern jedoch nicht. Fuer Projekte, die auf breite Kompatibilitaet angewiesen sind, bleibt eine MessageChannel-basierte Signalisierung als Fallback noetig, auch wenn diese fuer sehr feingranulare, haeufige Synchronisation deutlich mehr Overhead pro Nachricht verursacht.

In der Praxis lohnt sich Atomics.waitAsync() vor allem dort, wo mehrere Worker haeufig und mit geringer Latenz auf gemeinsamen Speicher synchronisieren muessen, etwa bei paralleler Bild- oder Audioverarbeitung. Fuer seltene, grobkoernige Koordination zwischen Worker und Main Thread ist eine einfache postMessage-Nachricht oft weiterhin die einfachere und ausreichend performante Wahl.

Mechanismus Blockiert Thread Nutzbar im Main Thread Typischer Einsatz
Atomics.wait() Ja, synchron bis Aufwachen Nein, wirft TypeError Dedizierter Worker mit strenger Reihenfolge
Atomics.waitAsync() Nein Ja Mutex/Semaphore zwischen mehreren Workern
postMessage / MessageChannel Nein Ja Seltene, grobkoernige Koordination
Busy-Spin-Loop (manuell) Nein, aber CPU-intensiv Technisch ja, aber unerwuenscht Nicht empfohlen, nur zu Testzwecken

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Atomics.waitAsync(): Das Wichtigste auf einen Blick

Kernidee

Atomics.waitAsync() wartet auf eine Speicheraenderung, ohne den ausfuehrenden Thread zu blockieren.

Unterschied zu wait()

Atomics.wait() blockiert synchron und ist im Main Thread verboten, waitAsync funktioniert ueberall.

Zusammenspiel

Atomics.notify() weckt wartende Threads gezielt auf, meist nach einem Atomics.store().

Voraussetzung

SharedArrayBuffer und damit auch waitAsync erfordern eine cross-origin-isolierte Seite.

11. FAQ: Atomics.waitAsync(): Das Wichtigste auf einen Blick

1Warum ist Atomics.wait() im Main Thread verboten?
Weil es den aufrufenden Thread synchron blockiert. Auf dem Main Thread wuerde das die gesamte Seite einfrieren, kein Scrollen, kein Klicken und keine Animation waeren mehr moeglich, bis das Warten beendet ist, deshalb wirft der Browser dort einen TypeError.
2Was bedeutet die Eigenschaft async im Rueckgabewert von waitAsync?
async gibt an, ob das Ergebnis synchron sofort feststand, weil der Wert bereits abwich, oder ob es asynchron ueber ein Promise geliefert wird. Bei async gleich true enthaelt value das Promise, bei false enthaelt value direkt den Ergebnis-String.
3Brauche ich SharedArrayBuffer zwingend fuer Atomics.waitAsync()?
Ja, Atomics-Operationen arbeiten auf typisierten Arrays ueber einem SharedArrayBuffer, weil nur dieser Speicher tatsaechlich zwischen mehreren Threads geteilt wird. Ein gewoehnlicher ArrayBuffer ist dafuer nicht geeignet.
4Wozu brauche ich Cross-Origin-Isolation fuer diesen Anwendungsfall?
SharedArrayBuffer wird aus Sicherheitsgruenden nur auf Seiten mit gesetzten Cross-Origin-Opener-Policy- und Cross-Origin-Embedder-Policy-Headern bereitgestellt, um Seitenkanalangriffe wie Spectre zu erschweren. Ohne diese Header ist der Konstruktor schlicht nicht vorhanden.
5Was macht Atomics.notify() genau?
Atomics.notify() weckt eine angegebene Anzahl an Threads auf, die an einer bestimmten Speicherposition per wait oder waitAsync warten. Es setzt selbst keinen Wert, deshalb muss der geteilte Wert vorher separat per Atomics.store() geaendert werden.
6Wie baue ich einen Mutex mit diesen Primitiven?
Ueber Atomics.compareExchange() wird atomar versucht, einen Sperrwert von frei auf gesperrt zu setzen. Gelingt das nicht, wartet der Thread per waitAsync auf die naechste Freigabe, statt in einer Schleife zu pollen, beim Freigeben setzt der Besitzer den Wert zurueck und ruft notify auf.
7Ist Atomics.waitAsync() schneller als eine Polling-Schleife?
Deutlich, weil eine Polling-Schleife durchgehend CPU-Zeit verbraucht, waehrend waitAsync den wartenden Thread beim Scheduler registriert und erst bei einer tatsaechlichen Zustandsaenderung wieder aktiv wird, was den Ressourcenverbrauch in Wartephasen nahezu auf null senkt.
8Kann ich waitAsync auch verwenden, ohne SharedArrayBuffer serverseitig auszuliefern?
Nein, ohne die passenden Cross-Origin-Isolation-Header verweigert der Browser den SharedArrayBuffer-Konstruktor komplett, unabhaengig davon, ob Atomics.waitAsync() selbst vom Browser unterstuetzt wird.
9Unterstuetzen alle modernen Browser Atomics.waitAsync()?
Aktuelle Chromium- und Firefox-Versionen unterstuetzen die Methode, in aelteren Browserversionen fehlt sie jedoch. Fuer breite Kompatibilitaet bleibt eine MessageChannel-basierte Signalisierung als Fallback sinnvoll.
10Wann lohnt sich waitAsync nicht, und eine einfache postMessage reicht aus?
Bei seltener, grobkoerniger Koordination zwischen Worker und Main Thread, etwa dem einmaligen Melden eines Ergebnisses, ist eine gewoehnliche postMessage-Nachricht oft einfacher zu verstehen und ausreichend performant, ohne die zusaetzliche Komplexitaet von SharedArrayBuffer und Cross-Origin-Isolation.