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.addpara incrementar o valor de forma atômica. No lado doworker.ts, o mesmoSharedArrayBufferpode 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 aoAtomics.
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.notifye o consumidor espera eficientemente usandoAtomics.wait.Atomics.waitnã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
postMessageeonmessagemais 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 ArrayBufferoupostMessagepodem ser mais fáceis de usar do queSharedArrayBuffer.
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
Int32Arraydevem 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
SharedArrayBuffercomo umArrayBuffernormal, ocorrerão condições de corrida. Escrever no mesmo índice a partir de múltiplas threads sem usarAtomicsé 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.