Concorrência e Multithreading em TypeScript: do A ao Z
No mundo moderno do desenvolvimento web, a performance das aplicações está em primeiro plano. TypeScript, sendo uma extensão fortemente tipada sobre JavaScript, oferece aos desenvolvedores ferramentas poderosas para trabalhar com assincronicidade e multithreading. Neste artigo, abordaremos os conceitos fundamentais de concorrência, aprenderemos como o Event Loop funciona e como usar Worker Threads para verdadeiro multithreading em Node.js.
1. Assincronicidade e Event Loop: a base da concorrência em TypeScript
Antes de falar sobre multithreading, é importante entender como TypeScript (e JavaScript) processa tarefas concorrentes. Na base está o Event Loop — um mecanismo que permite executar operações de entrada e saída não bloqueantes, sem criar novas threads.
Vejamos um exemplo simples de código assíncrono:
async function fetchData(url: string): Promise<string> { const response = await fetch(url); return response.text();}
async function main(): Promise<void> { console.log('Início do carregamento'); const data = await fetchData('https://api.example.com/data'); console.log('Dados recebidos:', data);}
main();console.log('O código continua executando');Neste exemplo, await não bloqueia a thread principal. Enquanto aguarda a resposta do servidor, o Event Loop continua processando outras tarefas, como a execução de console.log. Isso é concorrência — a capacidade de processar várias tarefas em um mesmo período de tempo, alternando entre elas.
2. Promises e async/await: gerenciamento de operações assíncronas
TypeScript oferece construções sintáticas convenientes para trabalhar com assincronicidade. Promises são objetos que representam o resultado futuro de uma operação assíncrona. A combinação async/await torna o código linear e legível.
Vejamos um cenário mais complexo — execução paralela de várias requisições:
async function loadMultipleUsers(userIds: number[]): Promise<User[]> { const promises = userIds.map(id => fetch(`/users/${id}`).then(res => res.json())); return Promise.all(promises);}
// Usoconst users = await loadMultipleUsers([1, 2, 3, 4, 5]);console.log(users); // Todos os usuários carregados em paraleloO método Promise.all inicia todas as promises simultaneamente, o que acelera significativamente a execução em comparação com o carregamento sequencial. Este é um exemplo ideal de concorrência: as tarefas são executadas "simultaneamente" dentro de uma única thread.
3. Verdadeiro multithreading: Worker Threads em Node.js
Embora a assincronicidade lide bem com operações de entrada e saída, ela não ajuda em tarefas intensivas de CPU (processamento de imagens, cálculos complexos, criptografia). Para esses casos, Node.js oferece Worker Threads — threads reais do sistema operacional.
Vejamos um exemplo de cálculo de fatorial em uma thread separada:
// worker.tsimport { parentPort } from 'worker_threads';
function factorial(n: number): number { if (n === 0) return 1; return n * factorial(n - 1);}
parentPort?.on('message', (n: number) => { const result = factorial(n); parentPort?.postMessage(result);});
// main.tsimport { Worker } from 'worker_threads';
function runWorker(n: number): Promise<number> { return new Promise((resolve, reject) => { const worker = new Worker('./worker.ts'); worker.postMessage(n); worker.on('message', resolve); worker.on('error', reject); });}
async function main() { const result = await runWorker(20); console.log('Fatorial de 20:', result);}
main();Cada Worker opera em sua própria thread, possui seu próprio Event Loop e é