Start · Sprachen · JavaScript · Referenz · Atomics.compareExchange()

Atomics.compareExchange()

Methode

Schreibt atomar <code>replacementValue</code> an <code>index</code>, aber nur wenn dort aktuell <code>expectedValue</code> steht, und gibt stets den alten Wert zurück.

seit JavaScript ES2017 Kategorie: method-property

Signatur

Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)

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

Typ
number|bigint
Beschreibung
Der alte Wert an 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));
7 12 12 12

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]);
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]);
420

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();
Initialisierung läuft Bereits initialisiert

// 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: expectedValue wird vor dem Vergleich in den Elementtyp umgewandelt. Bei Uint8Array gilt deshalb 256 als 0 und -1 als 255. Das kann unerwartete Treffer erzeugen.
  • Fehler: Die Methode wirft einen TypeError, wenn das Array kein erlaubter Integer-Typ ist oder bei BigInt-Arrays kein bigint übergeben wird. Bei einem ungültigen Index wirft sie einen RangeError.
  • 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() und Atomics.notify(). Auf dem Haupt-Thread des Browsers ist Atomics.wait() nicht erlaubt. Dort gibt es Atomics.waitAsync().
  • Kompatibilität und Sicherheit: Die Methode gibt es in allen modernen Browsern und in Node.js. SharedArrayBuffer steht im Browser aber nur in einem cross-origin isolated Kontext zur Verfügung. Dafür muss die Seite die Header Cross-Origin-Opener-Policy: same-origin und Cross-Origin-Embedder-Policy: require-corp senden (Schutz vor Spectre). Mit einem normalen ArrayBuffer funktioniert die Methode ebenfalls, bringt dann aber keinen Nutzen zwischen Threads.