O que Todo Dev Deve Saber sobre SharedArrayBuffer e Atomics: Memória Compartilhada entre Workers Já leu

SharedArrayBuffer: O Que É e Por Que Usar SharedArrayBuffer é um objeto JavaScript que permite compartilhar um buffer de memória entre múltiplos workers (threads). Diferentemente dos Web Workers comuns, que comunicam-se por cópia de dados (postMessage), o SharedArrayBuffer oferece acesso direto à mesma região de memória, eliminando overhead de serialização. Isso é essencial em aplicações CPU-intensivas, processamento de vídeo, simulações 3D e análise de grandes datasets em tempo real. A principal vantagem é o desempenho: múltiplos workers podem ler e modificar os mesmos dados simultaneamente sem cópias custosas. Porém, com grande poder vem grande responsabilidade — você herda problemas clássicos de programação paralela como race conditions e deadlocks. É aqui que entra a API Atomics. Configurando SharedArrayBuffer Criação Básica Para criar um SharedArrayBuffer, você instancia-o como um TypedArray. Diferentemente de ArrayBuffer comum, este é compartilhável entre contextos: Restrições de Segurança Por questões de segurança (mitigação de Spectre), navegadores modernos exigem headers específicos. O servidor deve retornar: Atomics: Operações Thread-Safe A

SharedArrayBuffer: O Que É e Por Que Usar

SharedArrayBuffer é um objeto JavaScript que permite compartilhar um buffer de memória entre múltiplos workers (threads). Diferentemente dos Web Workers comuns, que comunicam-se por cópia de dados (postMessage), o SharedArrayBuffer oferece acesso direto à mesma região de memória, eliminando overhead de serialização. Isso é essencial em aplicações CPU-intensivas, processamento de vídeo, simulações 3D e análise de grandes datasets em tempo real.

A principal vantagem é o desempenho: múltiplos workers podem ler e modificar os mesmos dados simultaneamente sem cópias custosas. Porém, com grande poder vem grande responsabilidade — você herda problemas clássicos de programação paralela como race conditions e deadlocks. É aqui que entra a API Atomics.

Configurando SharedArrayBuffer

Criação Básica

Para criar um SharedArrayBuffer, você instancia-o como um TypedArray. Diferentemente de ArrayBuffer comum, este é compartilhável entre contextos:

// main.js
const sharedBuffer = new SharedArrayBuffer(32); // 32 bytes
const sharedArray = new Int32Array(sharedBuffer);

// Inicializa dados
sharedArray[0] = 100;

// Cria e passa para worker
const worker = new Worker('worker.js');
worker.postMessage({ sharedBuffer });
// worker.js
self.onmessage = (event) => {
  const { sharedBuffer } = event.data;
  const sharedArray = new Int32Array(sharedBuffer);

  console.log('Valor inicial:', sharedArray[0]); // 100
  sharedArray[0] = 200; // Modifica diretamente na memória compartilhada
};

Restrições de Segurança

Por questões de segurança (mitigação de Spectre), navegadores modernos exigem headers específicos. O servidor deve retornar:

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

Atomics: Operações Thread-Safe

A API Atomics fornece métodos que garantem operações indivisíveis (atômicas) sobre SharedArrayBuffer, prevenindo race conditions. Os principais são load, store, compareExchange, wait e notify.

Operações Básicas Atômicas

// main.js
const sharedBuffer = new SharedArrayBuffer(8);
const sharedArray = new Int32Array(sharedBuffer);

// Escrita atômica
Atomics.store(sharedArray, 0, 42);

// Leitura atômica
const value = Atomics.load(sharedArray, 0);
console.log(value); // 42

// Compare-and-swap (atualiza se valor == esperado)
const wasUpdated = Atomics.compareExchange(sharedArray, 0, 42, 100);
console.log(wasUpdated); // 42 (valor anterior)
console.log(Atomics.load(sharedArray, 0)); // 100

Sem Atomics, leituras/escritas simples (sharedArray[0] = 42) não são garantidas como thread-safe em todas as arquiteturas. Atomics garante que a operação completa atomicamente antes de qualquer outra instrução interferir.

Wait e Notify: Sincronização Entre Threads

O padrão mais poderoso é usar wait() e notify() para sincronização produtor-consumidor:

// main.js (produtor)
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

const worker = new Worker('worker.js');
worker.postMessage({ sharedBuffer });

// Simula produção
setTimeout(() => {
  Atomics.store(sharedArray, 0, 42);
  Atomics.notify(sharedArray, 0); // Acorda workers aguardando
}, 1000);
// worker.js (consumidor)
self.onmessage = (event) => {
  const { sharedBuffer } = event.data;
  const sharedArray = new Int32Array(sharedBuffer);

  console.log('Esperando dados...');
  Atomics.wait(sharedArray, 0, 0); // Bloqueia até notify ou timeout

  const value = Atomics.load(sharedArray, 0);
  console.log('Recebi:', value); // 42
};

Atomics.wait() só funciona em Workers, nunca na thread principal (evita congelamento da UI). Quando notify() é chamado, todos os workers aguardando aquele índice são acordados.

Caso de Uso Prático: Pipeline de Processamento

Imagine processar frames de vídeo com múltiplos workers. Cada worker processa um quadro diferentes enquanto compartilham estado de progresso:

// main.js
const FRAME_COUNT = 100;
const sharedBuffer = new SharedArrayBuffer(4 * 3); // 3 Int32s
const state = new Int32Array(sharedBuffer); // [framesProcessadas, erros, pausado]

// Inicia 4 workers
const workers = Array.from({ length: 4 }, () => {
  const w = new Worker('frame-processor.js');
  w.postMessage({ sharedBuffer, totalFrames: FRAME_COUNT });
  return w;
});

// Monitor progress
setInterval(() => {
  const processed = Atomics.load(state, 0);
  console.log(`Progresso: ${processed}/${FRAME_COUNT}`);
}, 500);
// frame-processor.js
self.onmessage = (event) => {
  const { sharedBuffer, totalFrames } = event.data;
  const state = new Int32Array(sharedBuffer);

  for (let i = 0; i < totalFrames; i++) {
    // Processa frame
    processFrame(i);

    // Incremento atômico do contador
    Atomics.add(state, 0, 1);
  }
};

function processFrame(index) {
  // Simulação: processamento pesado
  let sum = 0;
  for (let i = 0; i < 1000000; i++) sum += Math.sqrt(i);
}

Conclusão

SharedArrayBuffer e Atomics são ferramentas poderosas para paralelismo real em JavaScript. Primeira aprendizagem: SharedArrayBuffer elimina cópias de dados, permitindo que múltiplos workers acessem a mesma memória. Segunda: Atomics garante operações thread-safe e fornece primitivas de sincronização (wait/notify). Terceira: use esse padrão apenas quando necessário — overhead de sincronização pode degradar performance em casos triviais. Domine esses conceitos e você dominará computação paralela em JavaScript.

Referências


Artigos relacionados