Il `SharedArrayBuffer` in TypeScript

Il `SharedArrayBuffer` in TypeScript

Questo articolo spiega il SharedArrayBuffer in TypeScript.

Spiegheremo SharedArrayBuffer in TypeScript con esempi pratici.

YouTube Video

Il SharedArrayBuffer in TypeScript

SharedArrayBuffer è un meccanismo per condividere lo stesso spazio di memoria tra più thread, come ad esempio i Web Workers. Combinandolo con Atomics, puoi gestire le race condition e svolgere operazioni di memoria condivisa a bassa latenza.

Prerequisiti e Note

Quando utilizzi SharedArrayBuffer nel browser, sono necessari gli header COOP e COEP per rispettare i requisiti di sicurezza noti come isolamento cross-origin. In Node.js, la memoria condivisa può essere gestita abbastanza facilmente utilizzando worker_threads.

Concetti di Base

SharedArrayBuffer è un oggetto che rappresenta una sequenza di byte a lunghezza fissa, e puoi leggere e scrivere numeri tramite TypedArray e viste simili. Le operazioni di lettura/scrittura semplici non sono sincronizzate, quindi si utilizza l'API Atomics per garantire operazioni atomiche e si usano i meccanismi wait e notify per la coordinazione.

Contatore semplice (versione browser)

In questo esempio, il thread principale crea un SharedArrayBuffer e lo passa a un Web Worker, e entrambi incrementano un contatore condiviso. Questo dimostra il pattern minimale: somma atomica con Atomics.add e lettura con Atomics.load.

main.ts (Lato browser)

Questo esempio mostra come il thread principale crea un SharedArrayBuffer e lo condivide con un Worker per l'accesso multi-thread.

 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 questo codice, il thread principale utilizza Atomics.add per incrementare il valore atomicamente. Dal lato worker.ts, lo stesso SharedArrayBuffer può essere accessibile e manipolato.

worker.ts (Worker del browser)

Questo è un esempio in cui il worker riceve lo stesso buffer condiviso e lo decrementa o manipola periodicamente.

 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};
  • Il Worker manipola la stessa memoria tramite Int32Array, e gli aggiornamenti avvengono senza race condition grazie a Atomics.

Sincronizzazione usando wait/notify

Utilizzando Atomics.wait e Atomics.notify, puoi sospendere i thread finché non vengono soddisfatte determinate condizioni, consentendo una sincronizzazione event-driven nei thread dei worker. Nei browser, l'approccio più sicuro è usare Atomics.wait all'interno di un Worker.

producer.ts (Worker produttore browser)

Il produttore scrive i dati e notifica il consumatore usando 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 (Worker consumatore browser)

Il consumatore attende con Atomics.wait e riprende l'elaborazione dopo la notifica del produttore.

 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 questo schema, il produttore notifica il consumatore tramite Atomics.notify e il consumatore attende in modo efficiente usando Atomics.wait. Atomics.wait non può essere chiamato nel thread principale nei browser. Per evitare il blocco dell'interfaccia, l'uso di Atomics.wait è limitato solo ai Worker.

Esempio pratico con Node.js (worker_threads)

In un ambiente Node.js, puoi gestire la funzionalità SharedArrayBuffer utilizzando worker_threads. Di seguito un esempio tipizzato di TypeScript per Node.js.

main-node.ts

Il thread principale crea il buffer e lo passa al 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

Questo esempio utilizza parentPort e workerData da worker_threads dal lato 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, non ci sono restrizioni COOP o COEP come nei browser, quindi la memoria condivisa può essere gestita facilmente semplicemente utilizzando worker_threads. Quando usi TypeScript, presta attenzione al fatto se stai compilando con impostazioni CommonJS o ESM.

Aspetti della tipizzazione in TypeScript

SharedArrayBuffer e Atomics sono inclusi nelle definizioni di tipi standard DOM e delle librerie, quindi puoi usarli direttamente in TypeScript. Quando scambi messaggi con i Worker, è più sicuro definire le interfacce e specificare chiaramente i tipi.

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • Definire esplicitamente i tipi rende la gestione di postMessage e onmessage più sicura e permette il type checking.

Casi d'uso pratici

SharedArrayBuffer non è sempre necessario, ma è altamente efficace nelle situazioni in cui sono richiesti i vantaggi di velocità della memoria condivisa. Capire in quali situazioni è efficace porta a scelte tecnologiche appropriate.

  • È adatto per elaborazioni a bassa latenza che richiedono buffer condivisi ad alta velocità e può essere usato per l'audio/video in tempo reale o la fisica dei giochi.
  • Per semplici scambi di dati o trasferimenti di grandi quantità di dati, Transferable ArrayBuffer o postMessage potrebbero essere più semplici da usare rispetto a SharedArrayBuffer.

Limitazioni e sicurezza

Per usare SharedArrayBuffer nel browser, è richiesto l'isolamento cross-origin: imposta COOP su same-origin-allow-popups e COEP su require-corp. SharedArrayBuffer sarà disabilitato se questi requisiti non vengono soddisfatti.

Suggerimenti su performance e debugging

Le operazioni atomiche (Atomics) sono rapide, ma attese frequenti e sincronizzazioni eccessive possono aumentare la latenza.

I seguenti punti dovrebbero essere verificati per gestire la memoria condivisa in modo sicuro ed efficiente.

  • Le viste come Int32Array dovrebbero essere gestite con un corretto allineamento dei byte.
  • Sii chiaro su quali indici dello stesso buffer condiviso sono usati da quali processi, e mantieni convenzioni coerenti nel codice.
  • Se tratti un SharedArrayBuffer come un normale ArrayBuffer, si verificheranno race condition. Scrivere nello stesso indice da più thread senza usare Atomics è pericoloso.

Riepilogo

Facendo un uso corretto di SharedArrayBuffer e Atomics, puoi ottenere operazioni di memoria condivisa sicure e rapide anche in TypeScript. Iniziare con schemi di sincronizzazione semplici come contatori o segnali è più facile da capire e, gestendo attentamente la sincronizzazione e gli indici corretti, puoi essere efficace anche in scenari a bassa latenza.

Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.

YouTube Video