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.addper incrementare il valore atomicamente. Dal latoworker.ts, lo stessoSharedArrayBufferpuò 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 aAtomics.
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.notifye il consumatore attende in modo efficiente usandoAtomics.wait.Atomics.waitnon può essere chiamato nel thread principale nei browser. Per evitare il blocco dell'interfaccia, l'uso diAtomics.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
postMessageeonmessagepiù 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 ArrayBufferopostMessagepotrebbero essere più semplici da usare rispetto aSharedArrayBuffer.
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
Int32Arraydovrebbero 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
SharedArrayBuffercome un normaleArrayBuffer, si verificheranno race condition. Scrivere nello stesso indice da più thread senza usareAtomicsè 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.