Der `SharedArrayBuffer` in TypeScript

Der `SharedArrayBuffer` in TypeScript

Dieser Artikel erklärt den SharedArrayBuffer in TypeScript.

Wir erklären SharedArrayBuffer in TypeScript anhand praktischer Beispiele.

YouTube Video

Der SharedArrayBuffer in TypeScript

SharedArrayBuffer ist ein Mechanismus, um denselben Speicherbereich zwischen mehreren Threads, wie zum Beispiel Web Workers, zu teilen. Durch die Kombination mit Atomics können Datenkonflikte vermieden und speichergeteilte Operationen mit niedriger Latenz durchgeführt werden.

Voraussetzungen und Hinweise

Beim Einsatz von SharedArrayBuffer im Browser müssen die COOP- und COEP-Header gesetzt sein, um die sogenannten Cross-Origin-Isolation Sicherheitsanforderungen zu erfüllen. Unter Node.js kann gemeinsam genutzter Speicher relativ einfach mithilfe von worker_threads behandelt werden.

Grundlegende Konzepte

SharedArrayBuffer ist ein Objekt, das eine Sequenz von Bytes fester Länge darstellt, wobei Zahlen über TypedArray und ähnliche Sichten gelesen und geschrieben werden können. Einfache Lese-/Schreiboperationen sind nicht synchronisiert. Daher wird die Atomics-API verwendet, um atomare Operationen zu gewährleisten und mit den Mechanismen wait und notify Koordination zu ermöglichen.

Einfacher Zähler (Browser-Version)

In diesem Beispiel erstellt der Hauptthread einen SharedArrayBuffer und übergibt ihn an einen Web Worker, wobei beide einen gemeinsamen Zähler erhöhen. Dies zeigt das minimale Muster: Atomare Addition mit Atomics.add und Lesen mit Atomics.load.

main.ts (Browser-Seite)

Dieses Beispiel zeigt, wie der Hauptthread einen SharedArrayBuffer erstellt und zur Mehrfachzugriff mit einem Worker teilt.

 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 diesem Code verwendet der Hauptthread Atomics.add, um den Wert atomar zu erhöhen. Auf der Seite von worker.ts kann auf denselben SharedArrayBuffer zugegriffen und dieser bearbeitet werden.

worker.ts (Browser-Worker)

Dies ist ein Beispiel, bei dem der Worker denselben gemeinsamen Speicher empfängt und ihn periodisch verringert oder verändert.

 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};
  • Der Worker verarbeitet auch denselben Speicher über ein Int32Array, und die Aktualisierungen erfolgen dank Atomics ohne Datenkonflikte (Race Conditions).

Synchronisation mit wait/notify

Durch den Einsatz von Atomics.wait und Atomics.notify können Threads angehalten werden, bis bestimmte Bedingungen erfüllt sind – so wird eine ereignisgesteuerte Synchronisation zwischen Worker-Threads ermöglicht. Im Browser ist es am sichersten, Atomics.wait innerhalb eines Workers zu verwenden.

producer.ts (Browser Producer Worker)

Der Producer schreibt Daten und benachrichtigt den Consumer mit 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)

Der Consumer wartet mit Atomics.wait und setzt nach der Benachrichtigung durch den Producer die Verarbeitung fort.

 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 diesem Muster benachrichtigt der Producer den Consumer über Atomics.notify und der Consumer wartet effizient mit Atomics.wait. Atomics.wait darf im Browser nicht im Hauptthread aufgerufen werden. Um das Einfrieren der Benutzeroberfläche zu verhindern, darf Atomics.wait nur innerhalb von Workern verwendet werden.

Praxisbeispiel mit Node.js (worker_threads)

In einer Node.js-Umgebung kann die Funktionalität von SharedArrayBuffer mit worker_threads genutzt werden. Nachfolgend ein typisiertes TypeScript-Beispiel für Node.js.

main-node.ts

Der Hauptthread erstellt den Buffer und übergibt ihn an den 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

In diesem Beispiel werden auf Worker-Seite parentPort und workerData aus worker_threads verwendet.

 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 gibt es keine COOP- oder COEP-Einschränkungen wie im Browser; gemeinsam genutzter Speicher kann einfach mit worker_threads verwaltet werden. Achten Sie bei der Verwendung von TypeScript darauf, ob Sie mit CommonJS- oder ESM-Einstellungen bauen.

TypeScript-Typisierungsaspekte

SharedArrayBuffer und Atomics sind in den Standard-Typdefinitionen von DOM und Bibliotheken enthalten und können direkt in TypeScript verwendet werden. Beim Nachrichtenaustausch mit Workern ist es sicherer, Schnittstellen (Interfaces) zu definieren und Typen klar anzugeben.

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • Durch die explizite Definition von Typen wird die Handhabung von postMessage und onmessage sicherer und Typüberprüfungen werden ermöglicht.

Praktische Anwendungsfälle

SharedArrayBuffer ist nicht immer notwendig, aber in Situationen, in denen die Geschwindigkeitsvorteile von gemeinsam genutztem Speicher benötigt werden, ist er äußerst effektiv. Das Verständnis der Situationen, in denen er effektiv ist, führt zu geeigneten Technologieentscheidungen.

  • Es eignet sich für latenzarme Verarbeitung, die schnelle gemeinsam genutzte Speicherbereiche erfordert, z.B. für Echtzeit-Audio-/Videobearbeitung oder Spielphysik.
  • Für einfachen Datenaustausch oder den Transfer großer Datenmengen ist ein Transferable ArrayBuffer oder postMessage oft leichter zu verwenden als ein SharedArrayBuffer.

Einschränkungen und Sicherheit

Um SharedArrayBuffer im Browser zu benutzen, ist Cross-Origin-Isolation erforderlich: COOP muss auf same-origin-allow-popups und COEP auf require-corp gesetzt werden. SharedArrayBuffer wird deaktiviert, wenn diese Anforderungen nicht erfüllt sind.

Leistungs- und Debugging-Tipps

Atomare Operationen (Atomics) sind schnell, aber häufiges Warten und übermäßige Synchronisation können die Latenz erhöhen.

Die folgenden Punkte sollten überprüft werden, um gemeinsam genutzten Speicher sicher und effizient zu handhaben.

  • Sichten wie Int32Array sollten mit der richtigen Byte-Ausrichtung verwendet werden.
  • Seien Sie eindeutig, welche Indizes des geteilten Speichers von welchen Prozessen verwendet werden, und halten Sie in Ihrem Code konsequente Konventionen ein.
  • Wenn Sie einen SharedArrayBuffer wie einen normalen ArrayBuffer behandeln, kann es zu Datenkonflikten kommen. Das Schreiben auf denselben Index aus mehreren Threads ohne Verwendung von Atomics ist gefährlich.

Zusammenfassung

Durch den richtigen Einsatz von SharedArrayBuffer und Atomics können Sie auch in TypeScript sichere und schnelle geteilte Speicheroperationen durchführen. Der Einstieg mit einfachen Synchronisationsmustern wie Zählern oder Signalen ist leichter verständlich. Durch sorgfältiges Management von Synchronisation und Indizes erreichen Sie auch in latenzarmen Szenarien eine hohe Effektivität.

Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.

YouTube Video