Bootcamp backendontwikkeling met Node.js · Les

Geheugen delen met SharedArrayBuffer en Atomics

Coördineer threads via gedeeld geheugen met Atomics om het kopiëren van grote buffers te voorkomen en raceconditions te vermijden.

Les 3 van 413 stappen

Geheugen delen met SharedArrayBuffer en Atomics is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met Node.js. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Waarom gedeeld geheugen?

Wanneer je een Worker start in Node.js en postMessage aanroept, worden de gegevens die je verstuurt gekopieerd met het structured-clone-algoritme. Voor kleine berichten is dat geen probleem, maar bij een grote numerieke buffer (miljoenen bytes) kost kopiëren onnodig CPU-tijd en geheugen.

  • SharedArrayBuffer laat meerdere threads hetzelfde geheugenblok lezen en schrijven zonder te kopiëren.
  • Atomics biedt veilige bewerkingen op dat geheugen, zonder dat er dataraces ontstaan.

In deze les leer je hoe je threads coördineert via gedeeld geheugen in een CPU-intensieve backendtaak.

ArrayBuffer versus SharedArrayBuffer

Een ArrayBuffer is eigendom van één thread. Wanneer je hem overdraagt aan een worker, heeft de verzender er geen toegang meer toe. Een SharedArrayBuffer (SAB) werkt anders: wanneer je hem aan een worker doorgeeft, deel je dezelfde onderliggende opslag, waardoor beide threads elkaars schrijfbewerkingen zien.

Je leest nooit rechtstreeks onbewerkte bytes. In plaats daarvan wikkel je de buffer in een weergave van een getypeerde array, zoals Int32Array of Float64Array.

const sab = new SharedArrayBuffer(16);
const view = new Int32Array(sab);

console.log(view.length);
view[0] = 42;
console.log(view[0]);
console.log(sab.byteLength);

Een SAB doorgeven aan een Worker

Als je geheugen wilt delen, maak je de SharedArrayBuffer aan in de hoofdthread en verstuur je hem via postMessage. In tegenstelling tot een normale buffer wordt een SAB gedeeld (niet overgedragen), zodat beide kanten hem kunnen blijven gebruiken.

De worker wikkelt dezelfde SAB in zijn eigen weergave van een getypeerde array. Er vindt geen kopieerbewerking plaats.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const sab = new SharedArrayBuffer(4);
  const view = new Int32Array(sab);
  view[0] = 100;
  new Worker(__filename, { workerData: sab });
} else {
  const view = new Int32Array(workerData);
  view[0] += 1;
  console.log('worker sees', view[0]);
}

Het probleem van dataraces

Gewone lees- en schrijfbewerkingen op een gedeelde weergave zijn niet veilig wanneer meerdere threads dezelfde positie gebruiken. Een instructie zoals view[0] += 1 bestaat in werkelijkheid uit drie stappen: lezen, optellen en schrijven. Twee threads kunnen elkaar afwisselen, waardoor updates verloren gaan.

  • Thread A leest 5 en thread B leest 5.
  • Beide berekenen 6 en schrijven 6.
  • Er vonden twee verhogingen plaats, maar de waarde is maar één hoger geworden.

Dit is een klassieke datarace. De oplossing is Atomics.

Atomics.add voor veilige tellers

Atomics.add(view, index, value) voert lezen, wijzigen en schrijven uit als één ondeelbare bewerking. Geen andere thread kan ertussen komen, waardoor verhogingen nooit verloren gaan.

Andere nuttige methoden zijn Atomics.sub, Atomics.and, Atomics.or en Atomics.load / Atomics.store voor gewone lees- en schrijfbewerkingen waarvan gegarandeerd is dat ze in alle threads zichtbaar zijn.

const sab = new SharedArrayBuffer(4);
const counter = new Int32Array(sab);

Atomics.store(counter, 0, 0);
Atomics.add(counter, 0, 5);
Atomics.add(counter, 0, 3);

console.log(Atomics.load(counter, 0));

CPU-werk verdelen over threads

Stel je voor dat je een enorme array met gehele getallen optelt, een CPU-intensieve taak die de eventloop zou blokkeren. Met gedeeld geheugen sla je de gegevens één keer op en laat je verschillende workers elk een deel verwerken. Ze schrijven hun gedeeltelijke resultaten via Atomics.add naar een gedeelde uitvoerpositie.

Omdat de invoer in een SharedArrayBuffer staat, hoef je de gegevensset nooit naar elke worker te kopiëren. Ze lezen allemaal dezelfde bytes.

Vergelijken en omwisselen

Atomics.compareExchange(view, index, expected, replacement) schrijft replacement alleen als de huidige waarde gelijk is aan expected, en retourneert de waarde die er stond. Dit vormt de basis voor vergrendelingsvrije algoritmen en eenvoudige spinlocks.

Gebruik dit om precies één keer beslag te leggen op een positie: als het omwisselen slaagt, heeft deze thread de positie geclaimd.

const sab = new SharedArrayBuffer(4);
const slot = new Int32Array(sab);
Atomics.store(slot, 0, 0);

const prev = Atomics.compareExchange(slot, 0, 0, 1);
console.log('previous value was', prev);
console.log('claimed:', prev === 0);

const again = Atomics.compareExchange(slot, 0, 0, 1);
console.log('second claim succeeded:', again === 0);

Blokkeren met Atomics.wait

Soms moet een worker pauzeren totdat een andere thread een signaal geeft. Atomics.wait(view, index, expectedValue) blokkeert de aanroepende thread zolang de positie nog expectedValue bevat. De methode retourneert 'ok', 'not-equal' of 'timed-out'.

  • Atomics.wait werkt alleen buiten de hoofdthread (anders zou de eventloop vastlopen).
  • Atomics.notify(view, index, count) maakt wachtende threads wakker.

Zo krijg je een echte threadbarrière zonder voortdurend te pollen.

Workers wakker maken met Notify

De producerthread werkt de gedeelde positie bij met Atomics.store en roept daarna Atomics.notify aan om threads wakker te maken die in Atomics.wait geparkeerd staan. De volgorde is belangrijk: wijzig eerst de waarde en geef daarna het signaal.

In een echte backendtaak is dit de manier waarop een coördinator alle workers tegelijk vrijgeeft om aan een fase te beginnen, of aangeeft dat de invoer gereed is.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const sab = new SharedArrayBuffer(4);
  const signal = new Int32Array(sab);
  Atomics.store(signal, 0, 0);
  new Worker(__filename, { workerData: sab });
  setTimeout(() => {
    Atomics.store(signal, 0, 1);
    Atomics.notify(signal, 0, 1);
  }, 50);
} else {
  const signal = new Int32Array(workerData);
  Atomics.wait(signal, 0, 0);
  console.log('worker released, value =', Atomics.load(signal, 0));
}

Een volledige parallelle optelling

Hier zie je het volledige patroon in één uitvoerbaar bestand: een gedeelde invoerbuffer, een gedeelde resultaatpositie en twee workers die elk de helft van de gegevens optellen en hun gedeeltelijke resultaat atomair bij het totaal optellen. De hoofdthread wacht totdat beide klaar zijn.

Let erop dat de invoer nooit wordt gekopieerd; beide workers lezen dezelfde SharedArrayBuffer.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const N = 1000;
  const dataSab = new SharedArrayBuffer(N * 4);
  const data = new Int32Array(dataSab);
  for (let i = 0; i < N; i++) data[i] = i + 1;

  const resultSab = new SharedArrayBuffer(8);
  const result = new Int32Array(resultSab);
  Atomics.store(result, 0, 0);
  Atomics.store(result, 1, 0);

  let done = 0;
  const ranges = [[0, N / 2], [N / 2, N]];
  for (const [start, end] of ranges) {
    const w = new Worker(__filename, { workerData: { dataSab, resultSab, start, end } });
    w.on('exit', () => {
      if (++done === ranges.length) {
        console.log('total =', Atomics.load(result, 0));
      }
    });
  }
} else {
  const { dataSab, resultSab, start, end } = workerData;
  const data = new Int32Array(dataSab);
  const result = new Int32Array(resultSab);
  let local = 0;
  for (let i = start; i < end; i++) local += data[i];
  Atomics.add(result, 0, local);
}

Praktische aandachtspunten

Gedeeld geheugen is krachtig, maar je kunt het gemakkelijk verkeerd gebruiken. Houd deze regels in gedachten:

  • SharedArrayBuffer slaat alleen getallen op. Als je tekenreeksen of objecten wilt delen, moet je ze coderen (bijvoorbeeld met TextEncoder naar een Uint8Array).
  • Gebruik altijd Atomics voor elke positie waar meer dan één thread naar kan schrijven; gewone schrijfbewerkingen combineren met atomaire schrijfbewerkingen zorgt opnieuw voor dataraces.
  • Reserveer een vaste positie voor synchronisatievlaggen en leg de index ervan vast.
  • Gebruik gedeeld geheugen alleen wanneer kopiëren echt een knelpunt vormt; voor de meeste berichten is gewone postMessage eenvoudiger en veilig.

Korte controle

Je hebt verschillende workerthreads die één gedeelde teller verhogen. Deze teller staat in een Int32Array die wordt ondersteund door een SharedArrayBuffer. Welke aanpak houdt de telling correct bij gelijktijdige uitvoering?

Samenvatting

Je hebt geleerd hoe je Node.js-workerthreads coördineert via gedeeld geheugen:

  • SharedArrayBuffer deelt één onderliggende opslag tussen threads zonder kopiëren; wikkel hem in een getypeerde array zoals Int32Array.
  • Gewoon += gebruiken op een gedeelde positie veroorzaakt dataraces; gebruik Atomics voor elke positie waar gelijktijdig naar wordt geschreven.
  • Atomics.add, Atomics.load, Atomics.store en Atomics.compareExchange bieden dataracevrije lees- en schrijfbewerkingen en vergrendelingsvrije claims.
  • Met Atomics.wait (buiten de hoofdthread) en Atomics.notify kunnen threads blokkeren en signalen geven zonder voortdurend te pollen.
  • Gebruik gedeeld geheugen pas wanneer het kopiëren van grote numerieke buffers echt een knelpunt vormt.
Gratis beginnen

Leer JavaScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
22
Lessen
92

Veelgestelde vragen

Is de les “Geheugen delen met SharedArrayBuffer en Atomics” gratis?

Ja — de volledige tekst van “Geheugen delen met SharedArrayBuffer en Atomics” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Bootcamp backendontwikkeling met Node.js wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Wat leer ik in “Geheugen delen met SharedArrayBuffer en Atomics”?

Coördineer threads via gedeeld geheugen met Atomics om het kopiëren van grote buffers te voorkomen en raceconditions te vermijden. Je oefent met Bootcamp backendontwikkeling met Node.js door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met Node.js te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met Node.js op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “Geheugen delen met SharedArrayBuffer en Atomics”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met Node.js?

Ja. Elke les over Bootcamp backendontwikkeling met Node.js bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Waarom de event loop stilvalt bij CPU-intensief werk
  2. Worker threads starten en berichten doorgeven
  3. Geheugen delen met SharedArrayBuffer en Atomics
  4. Een herbruikbare workerpool bouwen voor throughput
← Terug naar Bootcamp backendontwikkeling met Node.js