Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

SharedArrayBuffer: memoria condivisa tra thread in JavaScript

SharedArrayBuffer in JavaScript, come un buffer di memoria può essere condiviso tra il main thread e uno o più worker senza alcuna copia
SharedArrayBuffer in JavaScript, come un buffer di memoria può essere condiviso tra il main thread e uno o più worker senza alcuna copia
Link copiato negli appunti

In JavaScript i Transferable Objects risolvono il problema della copia, ma impongono un vincolo forte: un thread alla volta può accedere al dato. In alcuni scenari serve qualcosa di diverso: più thread che leggono e scrivono sulla stessa area di memoria simultaneamente. È qui che entra in gioco SharedArrayBuffer.

Cos'è SharedArrayBuffer in JavaScript

Un SharedArrayBuffer è un buffer di memoria che può essere condiviso tra il main thread e uno o più worker senza alcuna copia. Tutti i thread vedono lo stesso segmento fisico di memoria, e le modifiche apportate da un thread sono immediatamente visibili agli altri. Questo è il vero shared memory, lo stesso concetto che esiste nei thread POSIX o nei thread Java.

// main.js
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 4);
const arrayCondiviso = new Int32Array(sab);
// Inizializzazione dei valori
arrayCondiviso[0] = 100;
arrayCondiviso[1] = 200;
// Il worker riceve il buffer condiviso (non una copia!)
worker.postMessage({ buffer: sab });
// Dopo un po', il main thread legge il valore che il worker ha scritto
setTimeout(() => {
  console.log('Valore scritto dal worker:', arrayCondiviso[2]);
}, 1000);

// worker.js
self.onmessage = function(evento) {
  const arrayCondiviso = new Int32Array(evento.data.buffer);
  // Legge i valori scritti dal main thread
  const somma = arrayCondiviso[0] + arrayCondiviso[1];
  // Scrive il risultato nella posizione 2
  arrayCondiviso[2] = somma;
};

Il problema della race condition e perché è pericoloso ignorarlo

La memoria condivisa porta con sé il problema classico della programmazione concorrente: le race condition. Immagina due thread che leggono lo stesso valore, lo incrementano ciascuno per conto proprio, e lo riscrivono. Se le due scritture avvengono quasi simultaneamente, una sovrascriverà l'altra e il risultato finale sarà sbagliato. In JavaScript single-threaded questo problema non esiste per definizione. Con SharedArrayBuffer, invece, diventa reale.

La soluzione è l'oggetto Atomics, che fornisce operazioni atomiche sulla memoria condivisa. Un'operazione atomica è indivisibile: garantisce che nessun altro thread possa interferire mentre è in corso. Atomics.add, Atomics.store, Atomics.load, Atomics.compareExchange sono le principali primitive disponibili.

// Senza Atomics — pericoloso con più worker
arrayCondiviso[0] = arrayCondiviso[0] + 1; // NON atomico
// Con Atomics — sicuro con più thread concorrenti
Atomics.add(arrayCondiviso, 0, 1); // Legge, aggiunge 1 e scrive in modo atomico

Oltre alle operazioni di lettura/scrittura, Atomics fornisce anche meccanismi di sincronizzazione più avanzati. Atomics.wait mette un worker in attesa fino a quando un valore cambia, mentre Atomics.notify sveglia i worker in attesa su una certa posizione di memoria. Questo permette di costruire primitive di sincronizzazione come mutex e semafori direttamente in JavaScript.

// worker.js — attende finché la posizione 0 non cambia da 0
Atomics.wait(arrayCondiviso, 0, 0); // si blocca finché arrayCondiviso[0] !== 0
// Da qui in poi, sa che può procedere in modo sicuro

// main.js — segnala al worker che può procedere
Atomics.store(arrayCondiviso, 0, 1);
Atomics.notify(arrayCondiviso, 0, 1); // sveglia un worker in attesa

Requisiti di sicurezza: cross-origin isolation

SharedArrayBuffer è stato temporaneamente disabilitato da tutti i principali browser con supporto JavaScript nel 2018 come misura di mitigazione contro le vulnerabilità Spectre e Meltdown. L'accesso preciso a un timer ad alta risoluzione, combinato con memoria condivisa, poteva essere usato per inferire dati sensibili dalla cache della CPU attraverso attacchi side-channel. Da Chrome 92 e Firefox 79 è stato reintrodotto, ma con un requisito preciso: la pagina deve essere servita in un contesto cross-origin isolated.

In pratica bisogna configurare due header HTTP specifici sul server che serve la pagina. Il primo è Cross-Origin-Opener-Policy: same-origin, che isola la finestra del browser impedendo ad altre origini di accedervi tramite window.opener. Il secondo è Cross-Origin-Embedder-Policy: require-corp, che impedisce il caricamento di risorse cross-origin a meno che queste non dichiarino esplicitamente di voler essere incorporabili. Solo quando entrambi questi header sono presenti, la proprietà crossOriginIsolated della pagina sarà true e SharedArrayBuffer sarà disponibile.

# Configurazione Nginx
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
// Verifica runtime nel codice
if (self.crossOriginIsolated) {
  const sab = new SharedArrayBuffer(1024);
  // Possiamo procedere
} else {
  console.warn('SharedArrayBuffer non disponibile: contesto non isolato');
}

Un caso d'uso concreto: elaborazione di immagini in parallelo

Per dare concretezza a tutto quanto detto, consideriamo un caso reale: applicare un filtro di desaturazione (scala di grigi) a un'immagine ad alta risoluzione direttamente nel browser. Senza worker, questo calcolo blocca il thread principale per centinaia di millisecondi su immagini grandi. Con un worker dedicato, l'operazione avviene in background e l'interfaccia rimane reattiva.

// main.js
async function applicaFiltroGrigio(imageData) {
  const worker = new Worker('image-worker.js');
  // Trasferisce il buffer dei pixel al worker senza copia
  const buffer = imageData.data.buffer;
  return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
      const risultato = new ImageData(
        new Uint8ClampedArray(e.data.buffer),
        imageData.width,
        imageData.height
      );
      resolve(risultato);
      worker.terminate();
    };
    worker.onerror = reject;
    worker.postMessage(
      { buffer, width: imageData.width, height: imageData.height },
      [buffer] // trasferimento di proprietà
    );
  });
}

// image-worker.js
self.onmessage = function(e) {
  const { buffer, width, height } = e.data;
  const pixels = new Uint8ClampedArray(buffer);
  for (let i = 0; i < pixels.length; i += 4) {
    const media = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
    pixels[i] = media;      // R
    pixels[i + 1] = media;  // G
    pixels[i + 2] = media;  // B
    // pixels[i + 3] è l'alpha, lasciato invariato
  }
  self.postMessage({ buffer: pixels.buffer }, [pixels.buffer]);
};

In questo esempio il buffer dei pixel viene trasferito al worker (costo zero di copia), il filtro viene applicato iterando su tutti i pixel, e il buffer viene restituito al main thread con lo stesso meccanismo di trasferimento. L'utente non percepisce alcun blocco nell'interfaccia, indipendentemente dalle dimensioni dell'immagine.

Limitazioni e buone pratiche in JavaScript

I Web Workers non hanno accesso al DOM, e questo è intenzionale. Il DOM non è thread-safe: permettere a più thread di modificarlo simultaneamente creerebbe problemi di sincronizzazione difficilissimi da gestire. I worker sono quindi adatti al lavoro computazionale puro: calcoli numerici, trasformazioni di dati, parsing, crittografia, compressione. Per aggiornare l'interfaccia, il worker invia un messaggio al main thread, che si occupa della parte visuale.

Vale anche la pena ricordare che i worker hanno un costo di avvio non trascurabile. Creare un worker per ogni piccola operazione è controproducente. Il pattern consigliato è tenere in vita un pool di worker riutilizzabili, in modo da ammortizzare il costo di inizializzazione su molte operazioni nel tempo. Librerie come comlink di Google semplificano notevolmente la gestione della comunicazione tra thread, nascondendo il sistema a messaggi dietro una comoda interfaccia basata su Proxy e Promise.

Per quanto riguarda SharedArrayBuffer, il consiglio è di usarlo solo quando la comunicazione tramite messaggi è davvero insufficiente per ragioni di performance. La complessità aggiuntiva — gestione delle race condition, necessità di Atomics, requisiti di cross-origin isolation — è reale, e va giustificata da un beneficio misurabile. In tutti gli altri casi, il modello a messaggi di postMessage è più semplice, più sicuro, e ampiamente sufficiente.

Se vuoi aggiornamenti su SharedArrayBuffer: memoria condivisa tra thread in JavaScript inserisci la tua email nel box qui sotto:

Compilando il presente form acconsento a ricevere le informazioni relative ai servizi di cui alla presente pagina ai sensi dell'informativa sulla privacy.

Ti consigliamo anche