De `SharedArrayBuffer` in TypeScript

De `SharedArrayBuffer` in TypeScript

Dit artikel legt de SharedArrayBuffer in TypeScript uit.

We leggen SharedArrayBuffer in TypeScript uit aan de hand van praktische voorbeelden.

YouTube Video

De SharedArrayBuffer in TypeScript

SharedArrayBuffer is een mechanisme om dezelfde geheugenruimte te delen tussen meerdere threads, zoals Web Workers. Door het te combineren met Atomics kun je dataraces beheren en laag-latent gedeeld-geheugenoperaties uitvoeren.

Vereisten en opmerkingen

Bij gebruik van SharedArrayBuffer in de browser zijn COOP- en COEP-headers vereist om te voldoen aan beveiligingseisen, bekend als cross-origin isolation. In Node.js kan gedeeld geheugen relatief eenvoudig worden beheerd met behulp van worker_threads.

Basisconcepten

SharedArrayBuffer is een object dat een opeenvolging van bytes van vaste lengte voorstelt; je kunt getallen lezen en schrijven via TypedArray en vergelijkbare views. Eenvoudige lees/schrijfoperaties zijn niet gesynchroniseerd, dus gebruik je de Atomics API om atomaire operaties te garanderen en de wait- en notify-mechanismen voor coördinatie.

Eenvoudige teller (browserversie)

In dit voorbeeld maakt de hoofdthread een SharedArrayBuffer aan en geeft die door aan een Web Worker, waarbij beiden een gedeelde teller verhogen. Dit demonstreert het minimale patroon: atomaire optelling met Atomics.add en lezen met Atomics.load.

main.ts (browserzijde)

Dit voorbeeld laat zien hoe de hoofdthread een SharedArrayBuffer aanmaakt en deelt met een Worker voor multi-threaded toegang.

 1// main.ts
 2// Create a 4-byte (Int32) shared buffer for one counter
 3const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1);
 4const counter = new Int32Array(sab); // view over the buffer
 5
 6// Create worker and transfer the SharedArrayBuffer
 7const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
 8worker.postMessage({ sab });
 9
10// Increase counter in main thread every 200ms
11setInterval(() => {
12  const newVal = Atomics.add(counter, 0, 1) + 1; // Atomically increment
13  console.log('Main incremented ->', newVal);
14}, 200);
15
16// Listen for messages (optional)
17worker.onmessage = (e) => {
18  console.log('From worker:', e.data);
19};
  • In deze code gebruikt de hoofdthread Atomics.add om de waarde atomaire te verhogen. Aan de kant van worker.ts kan toegang worden verkregen tot hetzelfde SharedArrayBuffer en kan het worden gemanipuleerd.

worker.ts (Browser Worker)

Dit is een voorbeeld waarbij de worker dezelfde gedeelde buffer ontvangt en deze periodiek verlaagt of manipuleert.

 1// worker.ts
 2self.onmessage = (ev: MessageEvent) => {
 3  const { sab } = ev.data as { sab: SharedArrayBuffer };
 4  const counter = new Int32Array(sab);
 5
 6  // Every 350ms, atomically add 2 (demonstration)
 7  setInterval(() => {
 8    const newVal = Atomics.add(counter, 0, 2) + 2;
 9    // Optionally notify main thread (postMessage)
10    self.postMessage(`Worker added 2 -> ${newVal}`);
11  }, 350);
12};
  • De Worker manipuleert ook hetzelfde geheugen via Int32Array, en updates gebeuren zonder race conditions dankzij Atomics.

Synchronisatie met wait/notify

Door gebruik te maken van Atomics.wait en Atomics.notify kun je threads onderbreken tot aan bepaalde voorwaarden, waardoor event-gedreven synchronisatie tussen worker threads mogelijk wordt. In browsers is het veiligste om Atomics.wait binnen een Worker te gebruiken.

producer.ts (Browser Producer Worker)

De producer schrijft data en stelt de consument op de hoogte met notify.

 1// producer.ts (worker)
 2self.onmessage = (ev: MessageEvent) => {
 3  const { sab } = ev.data as { sab: SharedArrayBuffer };
 4  const state = new Int32Array(sab); // state[0] will be 0=empty,1=filled
 5
 6  // produce every 500ms
 7  setInterval(() => {
 8    // produce: set state to 1 (filled)
 9    Atomics.store(state, 0, 1);
10    Atomics.notify(state, 0, 1); // wake one waiter
11    self.postMessage('produced');
12  }, 500);
13};

consumer.ts (Browser Consumer Worker)

De consument wacht met Atomics.wait en hervat verwerking na melding van de producer.

 1// consumer.ts (worker)
 2self.onmessage = (ev: MessageEvent) => {
 3  const { sab } = ev.data as { sab: SharedArrayBuffer };
 4  const state = new Int32Array(sab);
 5
 6  // consumer loop
 7  (async function consumeLoop() {
 8    while (true) {
 9      // if state is 0 => wait until notified (value changes)
10      while (Atomics.load(state, 0) === 0) {
11        // Blocks this worker until notified or timeout
12        Atomics.wait(state, 0, 0);
13      }
14      // consume (reset to 0)
15      // (processing simulated)
16      // read data from another shared buffer in real scenarios
17      Atomics.store(state, 0, 0);
18      // continue loop
19      self.postMessage('consumed');
20    }
21  })();
22};
  • In dit patroon stelt de producer de consument op de hoogte via Atomics.notify en wacht de consument efficiënt met Atomics.wait. Atomics.wait kan niet worden aangeroepen op de hoofdthread in browsers. Om bevriezing van de gebruikersinterface te voorkomen, mag Atomics.wait alleen in Workers worden gebruikt.

Praktisch voorbeeld met Node.js (worker_threads)

In een Node.js-omgeving kun je de functionaliteit van SharedArrayBuffer beheren met behulp van worker_threads. Hieronder staat een getypeerd TypeScript-voorbeeld voor Node.js.

main-node.ts

De hoofdthread maakt de buffer aan en geeft deze door aan de Worker.

 1// main-node.ts
 2import { Worker } from 'worker_threads';
 3import path from 'path';
 4
 5// Create SharedArrayBuffer for one 32-bit integer
 6const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
 7const counter = new Int32Array(sab);
 8
 9// Spawn worker and pass sab via workerData
10const worker = new Worker(path.resolve(__dirname, 'worker-node.js'), {
11  workerData: { sab }
12});
13
14// Log messages from worker
15worker.on('message', (msg) => console.log('Worker:', msg));
16
17// Increment in main thread periodically
18setInterval(() => {
19  const val = Atomics.add(counter, 0, 1) + 1;
20  console.log('Main increment ->', val);
21}, 300);

worker-node.ts

Dit voorbeeld gebruikt parentPort en workerData uit worker_threads aan de kant van de Worker.

 1// worker-node.ts
 2import { parentPort, workerData } from 'worker_threads';
 3
 4const sab = workerData.sab as SharedArrayBuffer;
 5const counter = new Int32Array(sab);
 6
 7// Periodically add 5
 8setInterval(() => {
 9  const val = Atomics.add(counter, 0, 5) + 5;
10  parentPort?.postMessage(`Added 5 -> ${val}`);
11}, 700);
  • In Node.js zijn er geen COOP- of COEP-beperkingen zoals in browsers, dus gedeeld geheugen kan eenvoudig worden beheerd met alleen worker_threads. Let bij het gebruik van TypeScript op of je werkt met CommonJS- of ESM-instellingen.

TypeScript-typingspunten

SharedArrayBuffer en Atomics zijn opgenomen in de standaard DOM- en library-typedefinities, dus je kunt ze direct in TypeScript gebruiken. Bij het uitwisselen van berichten met Workers is het veiliger om interfaces te definiëren en types duidelijk te specificeren.

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • Het expliciet definiëren van types maakt postMessage- en onmessage-afhandeling veiliger en maakt typecontrole mogelijk.

Praktische gebruikstoepassingen

SharedArrayBuffer is niet altijd noodzakelijk, maar het is zeer effectief in situaties waar de snelheidsvoordelen van gedeeld geheugen vereist zijn. Het begrijpen van situaties waarin het effectief is, leidt tot de juiste technologische keuzes.

  • Het is geschikt voor low-latency verwerking die snelle gedeelde buffers vereist, bijvoorbeeld voor real-time audio-/videoverwerking of game-physics.
  • Voor eenvoudige gegevensuitwisseling of het overbrengen van grote hoeveelheden data kunnen Transferable ArrayBuffer of postMessage eenvoudiger zijn dan SharedArrayBuffer.

Beperkingen en beveiliging

Om SharedArrayBuffer in de browser te gebruiken, is cross-origin isolation vereist: stel COOP in op same-origin-allow-popups en COEP op require-corp. SharedArrayBuffer wordt uitgeschakeld als niet aan deze vereisten wordt voldaan.

Prestatie- en debugtips

Atomaire operaties (Atomics) zijn snel, maar vaak wachten en overmatige synchronisatie kunnen de latentie verhogen.

Controleer de volgende punten om gedeeld geheugen veilig en efficiënt te gebruiken.

  • Views zoals Int32Array moeten met de juiste byte-uitlijning worden behandeld.
  • Wees duidelijk welke indices van dezelfde gedeelde buffer door welke processen worden gebruikt, en houd consistente conventies in je code aan.
  • Als je een SharedArrayBuffer als een gewone ArrayBuffer behandelt, treden dataraces op. Naar dezelfde index schrijven vanuit meerdere threads zonder Atomics te gebruiken is gevaarlijk.

Samenvatting

Door goed gebruik te maken van SharedArrayBuffer en Atomics kun je veilige en snelle gedeelde-geheugenoperaties uitvoeren, zelfs in TypeScript. Beginnen met eenvoudige synchronisatiepatronen zoals tellers of signalen is makkelijker te begrijpen. Door een correcte synchronisatie en indexbeheer grondig toe te passen, kun je effectief zijn, zelfs in low-latency-situaties.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video