SharedArrayBuffer em JavaScript

SharedArrayBuffer em JavaScript

Este artigo explica o SharedArrayBuffer em JavaScript.

Forneceremos uma explicação detalhada sobre os fundamentos do SharedArrayBuffer, como utilizá-lo, casos de uso específicos e considerações de segurança.

YouTube Video

SharedArrayBuffer em JavaScript

O SharedArrayBuffer é uma ferramenta poderosa em JavaScript para compartilhamento de memória entre várias threads. Especialmente em combinação com Web Workers, ele permite o processamento paralelo, tornando-o eficaz para tarefas intensivas em computação e aplicações que exigem capacidades em tempo real.

O que é SharedArrayBuffer?

O SharedArrayBuffer fornece um buffer de memória em JavaScript que permite o compartilhamento de dados binários entre várias threads (principalmente Web Workers). Um ArrayBuffer regular exige cópia entre a thread principal e os trabalhadores, mas o SharedArrayBuffer permite o compartilhamento direto de memória sem cópia, melhorando significativamente o desempenho.

Recursos

  • Memória Compartilhada Permite que múltiplas threads trabalhem com o mesmo espaço de memória.
  • Melhoria de Desempenho Como a cópia pode ser omitida, a sobrecarga é reduzida ao processar grandes volumes de dados.
  • Sincronização de Threads Você pode usá-lo juntamente com Atomics para realizar a sincronização e evitar conflitos no acesso à memória.

Exemplo Básico de Uso

 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

Neste exemplo, criamos uma área de memória de 16 bytes usando o SharedArrayBuffer e tratamos essa área de memória como um Int32Array. Esse buffer de memória pode ser compartilhado entre várias threads.

Usando com Web Workers

O verdadeiro valor do SharedArrayBuffer é demonstrado quando usado em combinação com Web Workers. O código a seguir é um exemplo do uso de memória compartilhada entre a thread principal e um trabalhador.

Na Thread Principal

 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]);

No Lado do Trabalhador (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};
  • Neste exemplo, a thread principal cria um buffer compartilhado e o passa para o trabalhador. O trabalhador pode acessar esse buffer para ler e modificar valores. Dessa forma, os dados podem ser compartilhados entre threads sem cópia.

Confirmação de Atualização Bidirecional

Ao usar SharedArrayBuffer, tanto a thread principal quanto os workers podem ler e escrever na mesma memória, permitindo a confirmação de atualização bidirecional. Abaixo está um exemplo onde a thread principal define um valor, um worker altera esse valor, e então a thread principal verifica a atualização.

Na Thread Principal

 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};

No Lado do Trabalhador (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};
  • Neste exemplo, a thread principal escreve o valor 100 primeiro, e depois que o worker lê esse valor, ele o reescreve para 200. Após isso, o worker notifica a thread principal, e a thread principal lê novamente a memória compartilhada para confirmar a atualização. Dessa forma, combinando notificações, é possível confirmar a atualização de forma bidirecional.

Sincronização com Atomics

Ao usar memória compartilhada, é necessário ter cuidado com condições de competição de dados e inconsistências. Quando múltiplos threads acessam a mesma memória simultaneamente, podem ocorrer conflitos. Para evitar isso, o JavaScript utiliza o objeto Atomics para sincronização.

Por exemplo, para incrementar com segurança um contador com múltiplos threads, você pode usar Atomics para evitar conflitos.

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 o valor em um índice específico de forma atômica e retorna o novo valor. Essa operação é garantida para ser livre de conflitos com outros threads. Atomics.load também é utilizado para ler valores de maneira segura da memória compartilhada.

Espera e Notificação Usando Atomics.wait e Atomics.notify

Ao usar SharedArrayBuffer, há situações em que um worker precisa esperar até que uma certa condição seja atendida, e então outro worker deve notificar o worker em espera após cumprir essa condição. Nesses casos, Atomics.wait e Atomics.notify são úteis.

Atomics.wait bloqueia uma thread até que o valor em um índice específico da memória compartilhada mude, enquanto Atomics.notify notifica as threads em espera de que elas podem continuar. Isso permite espera e notificação seguras entre múltiplos workers. No entanto, Atomics.wait não pode ser usado na thread principal e está disponível apenas dentro de workers.

 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};
  • Na thread principal, um SharedArrayBuffer é criado como memória compartilhada e é convertido em um Int32Array com apenas um elemento. Esse único espaço inteiro é usado como um sinal para sincronizar entre os workers. Em seguida, dois workers são criados, e cada um recebe uma função usando a propriedade name: waiter (papel de espera) e notifier (papel de notificação). Por fim, o buffer compartilhado é passado para ambos os workers, e manipuladores onmessage são configurados para que as mensagens enviadas de cada worker possam ser recebidas.

No Lado do Trabalhador (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!
  • Neste exemplo, o worker waiter permanece em estado de espera usando Atomics.wait enquanto o valor no índice 0 for 0. Por outro lado, quando o worker notifier altera o valor para 123 com Atomics.store e chama Atomics.notify, o worker waiter será retomado e poderá obter o valor atualizado. Com isso, pode-se alcançar uma espera e notificação eficiente e segura entre threads.

Casos de Uso para SharedArrayBuffer

SharedArrayBuffer é especialmente útil para os seguintes casos de uso:.

  • Processamento em Tempo Real É adequado para aplicações que requerem baixa latência, como processamento de áudio e vídeo ou motores de jogos, onde os dados precisam ser compartilhados instantaneamente entre as threads.
  • Computação Paralela Ao processar grandes quantidades de dados simultaneamente com múltiplas threads, o uso de SharedArrayBuffer evita cópias de memória e pode melhorar o desempenho.
  • Aprendizado de Máquina Ao paralelizar tarefas como pré-processamento de dados e inferência, torna-se possível uma computação eficiente.

Considerações de Segurança

SharedArrayBuffer é um recurso poderoso, mas também apresenta riscos de segurança. Em particular, preocupações sobre ataques de canal lateral como o Spectre suspenderam temporariamente seu suporte. Para mitigar essa vulnerabilidade, os navegadores implementaram as seguintes medidas:.

  • Isolamento de Site Sites que permitem o uso de SharedArrayBuffer serão executados em um processo completamente isolado de outros sites.
  • Política de Recursos Entre Origens Para usar o SharedArrayBuffer, os cabeçalhos Cross-Origin-Opener-Policy e Cross-Origin-Embedder-Policy devem ser configurados corretamente.

Por exemplo, ao configurar cabeçalhos como os seguintes, o uso de SharedArrayBuffer se torna possível:.

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

Isso evita que recursos externos interfiram no conteúdo atual e aumenta a segurança.

Resumo

SharedArrayBuffer é uma ferramenta muito poderosa para compartilhar memória entre múltiplos threads. É uma tecnologia essencial para melhorar o desempenho, e seus efeitos são particularmente evidentes nos campos de processamento em tempo real e computação paralela. No entanto, também envolve riscos de segurança, por isso a configuração e a sincronização corretas são importantes.

Ao utilizar SharedArrayBuffer, você pode construir aplicações web mais avançadas e com maior desempenho.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video