O `SharedArrayBuffer` no TypeScript

O `SharedArrayBuffer` no TypeScript

Este artigo explica o SharedArrayBuffer no TypeScript.

Vamos explicar o SharedArrayBuffer em TypeScript com exemplos práticos.

YouTube Video

O SharedArrayBuffer no TypeScript

SharedArrayBuffer é um mecanismo para compartilhar o mesmo espaço de memória entre múltiplas threads, como Web Workers. Combinando com Atomics, é possível gerenciar concorrência de dados e realizar operações de memória compartilhada com baixa latência.

Pré-requisitos e Observações

Ao usar SharedArrayBuffer no navegador, os cabeçalhos COOP e COEP são necessários para atender aos requisitos de segurança conhecidos como isolamento entre origens. No Node.js, a memória compartilhada pode ser manipulada de forma relativamente fácil usando worker_threads.

Conceitos Básicos

SharedArrayBuffer é um objeto que representa uma sequência de bytes de comprimento fixo, permitindo ler e escrever números através de TypedArray e visualizações semelhantes. Operações simples de leitura/escrita não são sincronizadas, então você usa a API Atomics para garantir operações atômicas e mecanismos de wait e notify para coordenação.

Contador Simples (versão Navegador)

Neste exemplo, a thread principal cria um SharedArrayBuffer e o passa para um Web Worker, ambos incrementando um contador compartilhado. Isto demonstra o padrão mínimo: adição atômica com Atomics.add e leitura com Atomics.load.

main.ts (lado do Navegador)

Este exemplo mostra como a thread principal cria um SharedArrayBuffer e o compartilha com um Worker para acesso multi-threaded.

 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};
  • Neste código, a thread principal usa Atomics.add para incrementar o valor de forma atômica. No lado do worker.ts, o mesmo SharedArrayBuffer pode ser acessado e manipulado.

worker.ts (Worker do Navegador)

Este é um exemplo onde o worker recebe o mesmo buffer compartilhado e periodicamente o decrementa ou manipula.

 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};
  • O Worker também manipula a mesma memória através de Int32Array, e as atualizações são feitas sem condições de corrida graças ao Atomics.

Sincronização usando wait/notify

Usando Atomics.wait e Atomics.notify, você pode suspender threads até que certas condições sejam atendidas, permitindo a sincronização dirigida por eventos entre threads de workers. Nos navegadores, a abordagem mais segura é usar Atomics.wait dentro de um Worker.

producer.ts (Worker Produtor do Navegador)

O produtor escreve dados e notifica o consumidor 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 Consumidor do Navegador)

O consumidor aguarda com Atomics.wait e retoma o processamento ao ser notificado pelo produtor.

 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};
  • Neste padrão, o produtor notifica o consumidor via Atomics.notify e o consumidor espera eficientemente usando Atomics.wait. Atomics.wait não pode ser chamado na thread principal em navegadores. Para evitar o congelamento da interface do usuário, Atomics.wait é restrito ao uso somente dentro de Workers.

Exemplo Prático com Node.js (worker_threads)

Em um ambiente Node.js, você pode manipular a funcionalidade do SharedArrayBuffer usando worker_threads. Abaixo está um exemplo em TypeScript tipado para Node.js.

main-node.ts

A thread principal cria o buffer e o passa ao 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

Este exemplo usa parentPort e workerData do worker_threads no lado do 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);
  • No Node.js, não há restrições de COOP ou COEP como nos navegadores, então a memória compartilhada pode ser facilmente manipulada apenas usando worker_threads. Ao usar TypeScript, preste atenção se você está compilando com configurações CommonJS ou ESM.

Pontos de Tipagem no TypeScript

SharedArrayBuffer e Atomics estão incluídos nas definições de tipo padrão do DOM e da biblioteca, então você pode usá-los diretamente no TypeScript. Ao trocar mensagens com Workers, é mais seguro definir interfaces e especificar os tipos claramente.

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • Definir tipos explicitamente torna o uso de postMessage e onmessage mais seguro e permite a verificação de tipos.

Casos de Uso Práticos

SharedArrayBuffer nem sempre é necessário, mas é altamente eficaz em situações onde são exigidas as vantagens de velocidade da memória compartilhada. Entender as situações em que ele é eficaz leva a escolhas tecnológicas apropriadas.

  • É adequado para processamento de baixa latência que requer buffers compartilhados de alta velocidade e pode ser usado para processamento de áudio/vídeo em tempo real ou física de jogos.
  • Para trocas simples de dados ou transferência de grandes volumes, Transferable ArrayBuffer ou postMessage podem ser mais fáceis de usar do que SharedArrayBuffer.

Restrições e Segurança

Para usar SharedArrayBuffer no navegador, é necessário isolamento entre origens: defina COOP como same-origin-allow-popups e COEP como require-corp. SharedArrayBuffer será desabilitado se esses requisitos não forem atendidos.

Dicas de Desempenho e Depuração

Operações atômicas (Atomics) são rápidas, mas esperas frequentes e sincronização excessiva podem aumentar a latência.

Os seguintes pontos devem ser verificados para manipular memória compartilhada com segurança e eficiência.

  • Visualizações como Int32Array devem ser manipuladas com o alinhamento de bytes adequado.
  • Seja claro sobre quais índices do mesmo buffer compartilhado são usados por quais processos e mantenha convenções consistentes no seu código.
  • Se você tratar um SharedArrayBuffer como um ArrayBuffer normal, ocorrerão condições de corrida. Escrever no mesmo índice a partir de múltiplas threads sem usar Atomics é perigoso.

Resumo

Fazendo uso apropriado de SharedArrayBuffer e Atomics, é possível alcançar operações de memória compartilhada rápidas e seguras mesmo em TypeScript. Começar com padrões simples de sincronização como contadores ou sinais é mais fácil de entender, e gerenciando corretamente a sincronização e os índices, você pode ser eficaz mesmo em cenários de baixa latência.

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