Signatur
Beschreibung
Atomics.compareExchange() ist die klassische Compare-and-Swap-Operation (CAS) für JavaScript. Die Methode liest den Wert an der Position index eines Integer-Typed-Arrays, vergleicht ihn mit expectedValue und schreibt nur bei Gleichheit replacementValue hinein. Lesen, Vergleichen und Schreiben laufen als eine unteilbare Operation ab. Kein anderer Thread kann dazwischen auf die Speicherzelle zugreifen.
Der Rückgabewert ist immer der alte Wert an dieser Position, egal ob getauscht wurde oder nicht. Ob der Tausch geklappt hat, prüfst du also mit alterWert === expectedValue. Dieses Muster ist die Grundlage für Sperren ohne Blockieren (Spinlocks, Mutexe), „einmalig ausführen“-Flags und optimistische Aktualisierungen in Schleifen, sobald mehrere Worker sich einen SharedArrayBuffer teilen.
Nutze compareExchange(), wenn ein Schreibvorgang von einem bestimmten Vorzustand abhängt. Für einfache Additionen oder Zuweisungen sind Atomics.add(), Atomics.store() oder Atomics.exchange() einfacher. Für komplexe Berechnungen, die keine eigene Atomics-Methode haben (etwa Maximum oder Multiplikation), baust du mit compareExchange() eine CAS-Schleife.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $typedArray Pflicht | Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array|BigInt64Array|BigUint64Array | Ganzzahliges Typed Array, normalerweise auf einem SharedArrayBuffer. Float32Array, Float64Array und Uint8ClampedArray sind nicht erlaubt. |
|
| $index Pflicht | number | Position im Array, an der verglichen und getauscht wird. Liegt der Index außerhalb des Arrays, wird ein RangeError geworfen. |
|
| $expectedValue Pflicht | number|bigint | Der Wert, den du an der Position erwartest. Er wird vor dem Vergleich in den Elementtyp des Arrays umgewandelt (z. B. abgeschnitten oder modulo 2n). | |
| $replacementValue Pflicht | number|bigint | Der neue Wert, der nur bei Gleichheit geschrieben wird. Bei BigInt64Array/BigUint64Array muss er ein bigint sein. |
Rückgabewert
typedArray[index] vor der Operation. Ist er gleich dem (umgewandelten) expectedValue, wurde getauscht.Beispiele
Grundprinzip: Tausch nur bei passendem Wert
const sab = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
const ta = new Int32Array(sab);
ta[0] = 7;
// Erwartung stimmt: 7 wird durch 12 ersetzt
console.log(Atomics.compareExchange(ta, 0, 7, 12));
console.log(Atomics.load(ta, 0));
// Erwartung stimmt nicht: nichts passiert
console.log(Atomics.compareExchange(ta, 0, 7, 99));
console.log(Atomics.load(ta, 0));
Einfacher Spinlock-Mutex zwischen Workern
const UNLOCKED = 0;
const LOCKED = 1;
class SpinLock {
constructor(int32, index = 0) {
this.cell = int32;
this.index = index;
}
lock() {
// So lange versuchen, bis wir 0 -> 1 tauschen konnten
while (Atomics.compareExchange(this.cell, this.index, UNLOCKED, LOCKED) !== UNLOCKED) {
// Im Worker: schlafen, bis jemand den Lock freigibt
Atomics.wait(this.cell, this.index, LOCKED);
}
}
unlock() {
Atomics.store(this.cell, this.index, UNLOCKED);
Atomics.notify(this.cell, this.index, 1);
}
}
// Nutzung (in jedem Worker mit demselben SharedArrayBuffer):
const shared = new Int32Array(new SharedArrayBuffer(8));
const mutex = new SpinLock(shared, 0);
mutex.lock();
shared[1] += 1; // kritischer Abschnitt
mutex.unlock();
console.log(shared[1]);
CAS-Schleife: atomares Maximum berechnen
const atomicMax = (ta, index, value) => {
let current = Atomics.load(ta, index);
while (value > current) {
const previous = Atomics.compareExchange(ta, index, current, value);
if (previous === current) return value; // Tausch erfolgreich
current = previous; // Ein anderer Thread war schneller, neu versuchen
}
return current;
};
const highscore = new Int32Array(new SharedArrayBuffer(4));
atomicMax(highscore, 0, 420);
atomicMax(highscore, 0, 300);
console.log(highscore[0]);
Einmal-Initialisierung mit BigInt64Array
const flags = new BigInt64Array(new SharedArrayBuffer(8));
const initOnce = () => {
// Nur der erste Aufrufer tauscht 0n -> 1n
if (Atomics.compareExchange(flags, 0, 0n, 1n) === 0n) {
console.log('Initialisierung läuft');
} else {
console.log('Bereits initialisiert');
}
};
initOnce();
initOnce();
// Wichtig · Fallstricke
- Rückgabewert richtig lesen: Zurückgegeben wird der alte Wert und kein Boolean. Den Erfolg prüfst du mit
result === expectedValue. - Typumwandlung beim Vergleich:
expectedValuewird vor dem Vergleich in den Elementtyp umgewandelt. BeiUint8Arraygilt deshalb256als0und-1als255. Das kann unerwartete Treffer erzeugen. - Fehler: Die Methode wirft einen
TypeError, wenn das Array kein erlaubter Integer-Typ ist oder bei BigInt-Arrays keinbigintübergeben wird. Bei einem ungültigen Index wirft sie einenRangeError. - ABA-Problem: CAS erkennt nicht, ob ein Wert zwischenzeitlich geändert und dann wieder auf den alten Wert zurückgesetzt wurde. Falls das eine Rolle spielt, kombiniere den Wert mit einem Versionszähler.
- Aktives Warten: Reine Spin-Schleifen verbrauchen viel CPU-Zeit. Kombiniere sie in Workern mit
Atomics.wait()undAtomics.notify(). Auf dem Haupt-Thread des Browsers istAtomics.wait()nicht erlaubt. Dort gibt esAtomics.waitAsync(). - Kompatibilität und Sicherheit: Die Methode gibt es in allen modernen Browsern und in Node.js.
SharedArrayBuffersteht im Browser aber nur in einem cross-origin isolated Kontext zur Verfügung. Dafür muss die Seite die HeaderCross-Origin-Opener-Policy: same-originundCross-Origin-Embedder-Policy: require-corpsenden (Schutz vor Spectre). Mit einem normalenArrayBufferfunktioniert die Methode ebenfalls, bringt dann aber keinen Nutzen zwischen Threads.