SharedArrayBuffer in JavaScript

SharedArrayBuffer in JavaScript

Questo articolo spiega il SharedArrayBuffer in JavaScript.

Forniremo una spiegazione dettagliata delle basi di SharedArrayBuffer, come utilizzarlo, casi d'uso specifici e considerazioni sulla sicurezza.

YouTube Video

SharedArrayBuffer in JavaScript

SharedArrayBuffer è uno strumento potente in JavaScript per condividere la memoria tra più thread. Specialmente in combinazione con i Web Worker, consente l'elaborazione parallela, rendendolo efficace per compiti computazionalmente intensivi e applicazioni che richiedono capacità in tempo reale.

Cos'è SharedArrayBuffer?

SharedArrayBuffer fornisce un buffer di memoria in JavaScript che consente la condivisione di dati binari tra più thread (principalmente Web Worker). Un ArrayBuffer normale richiede la copia di dati tra il thread principale e i worker, mentre SharedArrayBuffer consente la condivisione diretta della memoria senza copie, migliorando significativamente le prestazioni.

Caratteristiche

  • Memoria condivisa Permette a più thread di lavorare con lo stesso spazio di memoria.
  • Miglioramento delle prestazioni Poiché è possibile evitare la copia, il sovraccarico è ridotto quando si elaborano grandi quantità di dati.
  • Sincronizzazione dei thread Puoi utilizzarlo insieme a Atomics per effettuare la sincronizzazione e prevenire conflitti durante l'accesso alla memoria.

Esempio base di utilizzo

 1// Create a 16-byte shared memory
 2const sharedBuffer = new SharedArrayBuffer(16);
 3
 4// Treat it as an Int32Array
 5const sharedArray = new Int32Array(sharedBuffer);
 6
 7// Set a value
 8sharedArray[0] = 42;
 9
10console.log(sharedArray[0]);  // 42

In questo esempio, creiamo un'area di memoria di 16 byte utilizzando SharedArrayBuffer e trattiamo quella memoria come un Int32Array. Questo buffer di memoria può essere condiviso tra più thread.

Utilizzo con i Web Worker

Il vero valore di SharedArrayBuffer si dimostra quando viene utilizzato in combinazione con i Web Worker. Il seguente codice è un esempio di utilizzo della memoria condivisa tra il thread principale e un worker.

Nel Thread Principale

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Modify the memory
12// Output : Main thread: 100
13sharedArray[0] = 100;
14console.log("Main thread: ", sharedArray[0]);

Sul Lato del Worker (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    // Use the received shared buffer
 4    const sharedArray = new Int32Array(event.data);
 5
 6    // Read the contents of the memory
 7    // Output : Worker thread: 100
 8    console.log("Worker thread: ", sharedArray[0]);
 9
10    // Change the value
11    sharedArray[0] = 200;
12};
  • In questo esempio, il thread principale crea un buffer condiviso e lo passa al worker. Il worker può accedere a questo buffer per leggere e modificare i valori. In questo modo, i dati possono essere condivisi tra i thread senza copie.

Conferma di aggiornamento bidirezionale

Utilizzando SharedArrayBuffer, sia il thread principale che i worker possono leggere e scrivere sulla stessa memoria, consentendo la conferma di aggiornamenti bidirezionali. Di seguito è riportato un esempio in cui il thread principale imposta un valore, un worker modifica questo valore e poi il thread principale verifica l'aggiornamento.

Nel Thread Principale

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Set initial value
12// Output : Main thread initial: 100
13sharedArray[0] = 100;
14console.log("Main thread initial:", sharedArray[0]);
15
16// Listen for worker confirmation
17worker.onmessage = () => {
18    // Output : Main thread after worker update: 200
19    console.log("Main thread after worker update:", sharedArray[0]);
20};

Sul Lato del Worker (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    // Read initial value
 6    // Output : Worker thread received: 100
 7    console.log("Worker thread received:", sharedArray[0]);
 8
 9    // Update the value
10    sharedArray[0] = 200;
11
12    // Notify main thread
13    self.postMessage("Value updated");
14};
  • In questo esempio, il thread principale scrive prima il valore 100, e dopo che il worker lo legge, lo riscrive a 200. Successivamente, il worker notifica il thread principale, che legge di nuovo la memoria condivisa per confermare l’aggiornamento. In questo modo, combinando le notifiche, si rende possibile la conferma degli aggiornamenti bidirezionali.

Sincronizzazione con Atomics

Quando si utilizza la memoria condivisa, è necessario prestare attenzione alle condizioni di competizione sui dati e alle incoerenze. Quando più thread accedono contemporaneamente alla stessa memoria, possono verificarsi conflitti. Per prevenire ciò, JavaScript utilizza l'oggetto Atomics per la sincronizzazione.

Ad esempio, per incrementare in modo sicuro un contatore con più thread, è possibile utilizzare Atomics per prevenire conflitti.

1const sharedBuffer = new SharedArrayBuffer(16);
2const sharedArray = new Int32Array(sharedBuffer);
3
4// Increment the counter
5Atomics.add(sharedArray, 0, 1);
6
7console.log(Atomics.load(sharedArray, 0));  // 1

Atomics.add incrementa atomicamente il valore a un indice specifico e restituisce il nuovo valore. Questa operazione è garantita senza conflitti con altri thread. Atomics.load è utilizzato anche per leggere in modo sicuro i valori dalla memoria condivisa.

Attesa e notifica tramite Atomics.wait e Atomics.notify

Quando si utilizza SharedArrayBuffer, ci sono situazioni in cui un worker deve attendere che una certa condizione sia soddisfatta, e quando un altro worker soddisfa tale condizione, deve notificare il worker in attesa. In questi casi, Atomics.wait e Atomics.notify sono utili.

Atomics.wait blocca un thread finché il valore in un indice specifico della memoria condivisa non cambia, mentre Atomics.notify avvisa i thread in attesa che possono procedere. Ciò consente un’attesa e una notifica sicure tra più worker. Tuttavia, Atomics.wait non può essere utilizzato nel thread principale ed è disponibile solo all'interno dei worker.

 1// Create a shared buffer (1 Int32 slot is enough for signaling)
 2const sharedBuffer = new SharedArrayBuffer(4);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create workers with names
 6const waiter = new Worker('worker.js', { name: 'waiter' });
 7const notifier = new Worker('worker.js', { name: 'notifier' });
 8
 9// Pass the shared buffer to both
10waiter.postMessage(sharedBuffer);
11notifier.postMessage(sharedBuffer);
12
13// Listen for messages
14waiter.onmessage = (event) => {
15    console.log(`[Main] Message from waiter:`, event.data);
16};
17notifier.onmessage = (event) => {
18    console.log(`[Main] Message from notifier:`, event.data);
19};
  • Nel thread principale, un SharedArrayBuffer viene creato come memoria condivisa e viene convertito in un Int32Array con un solo elemento. Questo unico spazio per un intero viene utilizzato come segnale per la sincronizzazione tra i worker. Successivamente, vengono creati due worker e a ciascuno viene assegnato un ruolo utilizzando la proprietà name: waiter (il ruolo in attesa) e notifier (il ruolo di notifica). Infine, il buffer condiviso viene passato a entrambi i worker e vengono impostati i gestori onmessage in modo che i messaggi inviati da ciascun worker possano essere ricevuti.

Sul Lato del Worker (worker.js)

 1// worker.js
 2onmessage = (event) => {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    if (self.name === 'waiter') {
 6        postMessage('Waiter is waiting...');
 7        // Wait until notifier signals index 0
 8        Atomics.wait(sharedArray, 0, 0);
 9        postMessage('Waiter was notified!');
10    }
11
12    if (self.name === 'notifier') {
13        postMessage('Notifier is preparing...');
14        setTimeout(() => {
15            // Notify waiter after 2 seconds
16            Atomics.store(sharedArray, 0, 1);
17            Atomics.notify(sharedArray, 0, 1);
18            postMessage('Notifier has sent the signal!');
19        }, 2000);
20    }
21};
22// Output
23// [Main] Message from waiter: Waiter is waiting...
24// [Main] Message from notifier: Notifier is preparing...
25// [Main] Message from notifier: Notifier has sent the signal!
26// [Main] Message from waiter: Waiter was notified!
  • In questo esempio, il worker waiter rimane in stato di attesa utilizzando Atomics.wait finché il valore all'indice 0 è 0. D'altra parte, quando il worker notifier cambia il valore a 123 con Atomics.store e chiama Atomics.notify, il worker waiter riprenderà e potrà ottenere il valore aggiornato. Con questo si può ottenere un'attesa ed una notifica efficiente e sicura tra i thread.

Casi d'uso per SharedArrayBuffer

SharedArrayBuffer è particolarmente utile per i seguenti casi d'uso:.

  • Elaborazione in tempo reale È adatto per applicazioni che richiedono bassa latenza, come l’elaborazione audio/video o i motori di gioco, dove i dati devono essere condivisi istantaneamente tra i thread.
  • Calcolo parallelo Quando si elaborano grandi quantità di dati contemporaneamente con più thread, l’uso di SharedArrayBuffer evita la copia della memoria e può migliorare le prestazioni.
  • Apprendimento automatico Parallelizzando attività come il pre-processamento dei dati e l’inferenza, è possibile ottenere un’elaborazione efficiente.

Considerazioni sulla sicurezza

SharedArrayBuffer è una funzionalità potente, ma comporta anche rischi per la sicurezza. In particolare, preoccupazioni riguardanti attacchi di tipo side-channel come Spectre ne hanno temporaneamente sospeso il supporto. Per mitigare questa vulnerabilità, i browser hanno implementato le seguenti misure:.

  • Isolamento del sito I siti che consentono l’uso di SharedArrayBuffer verranno eseguiti in un processo completamente isolato dagli altri siti.
  • Cross-Origin Resource Policy Per utilizzare SharedArrayBuffer, le intestazioni Cross-Origin-Opener-Policy e Cross-Origin-Embedder-Policy devono essere impostate correttamente.

Ad esempio, configurando header come i seguenti, l'uso di SharedArrayBuffer diventa possibile:.

1Cross-Origin-Opener-Policy: same-origin
2Cross-Origin-Embedder-Policy: require-corp

Questo impedisce alle risorse esterne di interferire con i contenuti correnti e aumenta la sicurezza.

Riepilogo

SharedArrayBuffer è uno strumento molto potente per la condivisione della memoria tra più thread. È una tecnologia essenziale per migliorare le prestazioni, e i suoi effetti sono particolarmente evidenti nei campi dell'elaborazione in tempo reale e del calcolo parallelo. Tuttavia, comporta anche rischi per la sicurezza, quindi una configurazione e una sincronizzazione corrette sono importanti.

Sfruttando SharedArrayBuffer, puoi creare applicazioni web più avanzate e ad alte prestazioni.

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

YouTube Video