Introdução 🧐
O multi-threading é uma técnica comum para melhorar a performance de aplicativos, mas não é isenta de desafios. Este artigo se propõe a explorar, de maneira objetiva, as circunstâncias sob as quais o multi-threading pode não oferecer os benefícios esperados, utilizando um exemplo prático em Node.js.
Contextualização
A capacidade de executar várias threads simultaneamente pode, teoricamente, reduzir o tempo de execução de tarefas complexas. No entanto, a realidade é que o overhead de gerenciamento de threads e a contenção de recursos podem impactar negativamente a performance.
Exemplo Prático 👩🏾💻
Para ilustrar isso, consideremos um exemplo específico em Node.js, onde o objetivo é calcular números primos. O código abaixo foi utilizado para comparar a eficiência entre a execução single-threaded e multi-threaded.
const {
Worker,
isMainThread,
parentPort,
workerData } = require('worker_threads');
const { performance } = require('perf_hooks');
// Função para encontrar números primos em um intervalo
const findPrimes = (start, end) => {
const primes = [];
// Itera por todos os números no intervalo [start, end]
for (let i = start; i <= end; i++) {
let isPrime = true;
// Verifica se o número 'i' é divisível por algum número
// entre 2 e sua raiz quadrada
for (let j = 2, sqrt = Math.sqrt(i); j <= sqrt; j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
// Se 'i' for primo (e maior que 1), adiciona-o ao
// array de primos
if (isPrime && i > 1) {
primes.push(i);
}
}
return primes;
}
// Função para calcular números primos em um único thread
function singleThread(max) {
// Marca o início da medição de tempo
const stStart = performance.now();
// Chama a função para encontrar números primos no
// intervalo [2, max]
findPrimes(2, max);
// Marca o final da medição de tempo
const stEnd = performance.now();
// Imprime o tempo decorrido em milissegundos
console.log('Single-threaded:', stEnd - stStart, 'ms');
}
// Função para calcular números primos em vários threads
function multiThread(max, numbersPerThread, threadCount) {
const mtStart = performance.now(); // Marca o início da medição de tempo
let completed = 0;
for (let i = 0; i < threadCount; i++) {
const start = i * numbersPerThread + 2;
const end = i === threadCount - 1 ? max : start + numbersPerThread - 1;
// Cria um novo thread para calcular primos em um subintervalo
const worker = new Worker(__filename, {
workerData: { start, end }
});
// Quando o thread conclui, incrementa o contador 'completed'
// e verifica se todos os threads terminaram para medir o tempo total
worker.on('message', () => {
completed++;
if (completed === threadCount) {
// Marca o final da medição de tempo quando todos os threads terminam
const mtEnd = performance.now();
// Imprime o tempo decorrido em milissegundos
console.log('Multi-threaded:', mtEnd - mtStart, 'ms');
}
});
}
}
// Se estiver no thread principal
if (isMainThread) {
// Define o número máximo no intervalo
const max = 1e5;
// Define o número total de threads a serem usadas
const threadCount = 4;
// Calcula quantos números cada thread deve processar
const numbersPerThread = Math.ceil(max / threadCount);
// Calcula primos em um único thread
singleThread(max);
// Calcula primos em múltiplos threads
multiThread(max, numbersPerThread, threadCount);
} else {
// Se estiver em um thread
// Recebe os dados de start e end do intervalo a ser processado
const { start, end } = workerData;
// Chama a função para encontrar primos no intervalo especificado
findPrimes(start, end);
// Envia uma mensagem de conclusão após o cálculo
parentPort.postMessage(true);
}
Quando você executar o script completo, os resultados que você irá obter serão parecidos com:
`
Single-threaded: 17.95 ms
Multi-threaded: 75.41 ms`
😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱😱
Contrariando as expectativas iniciais, a execução multi-threaded foi mais lenta.
Análise 👀
A razão para isso pode ser atribuída ao overhead associado à criação e gerenciamento de threads. Cada thread requer recursos para ser iniciada, executada e, posteriormente, encerrada. Em tarefas que não são intensivamente paralelizáveis ou que são relativamente rápidas, o custo de gerenciamento de threads pode superar os benefícios do processamento paralelo.
Além disso, a contenção de recursos, onde várias threads competem pelos mesmos recursos do sistema, pode levar a um aumento no tempo de execução, especialmente em sistemas com recursos limitados.
Considerações
É essencial, portanto, avaliar a adequação do multi-threading para cada tarefa específica. Fatores como a natureza da tarefa, o ambiente de execução e os recursos disponíveis desempenham um papel crucial na determinação da eficácia do multi-threading.
A Necessidade de Benchmarking
Um passo essencial e muitas vezes subestimado na implementação do multi-threading é a realização de testes de benchmark rigorosos. Estes testes não são apenas uma formalidade, mas uma necessidade para avaliar objetivamente o impacto real na performance. Os desenvolvedores devem se equipar com dados concretos para tomar decisões informadas, evitando suposições e generalizações.
Volume de Dados: Um Fator Crítico
O volume de dados é um elemento inseparável desta discussão. Testes de benchmark realizados com um volume de dados baixo podem não refletir a realidade operacional e, consequentemente, oferecer uma visão distorcida da eficácia do multi-threading. É imperativo que os testes sejam conduzidos em um ambiente que simule as condições reais de operação, com volumes de dados significativos.
A razão para isso é clara: o overhead de gerenciamento de threads pode ser minimizado ou justificado quando lidamos com volumes de dados grandes. Em contrapartida, para volumes de dados menores, o custo de inicialização, execução e terminação de threads pode superar os benefícios percebidos, como observado em nosso exemplo prático.
Recomendação Final
Portanto, a recomendação é clara. Antes de embarcar na jornada do multi-threading, invista tempo e recursos em testes de benchmark detalhados, com um volume de dados que reflita as condições reais de uso. Este passo é fundamental para garantir que a implementação de multi-threading traga benefícios tangíveis em termos de eficiência e performance, e para evitar surpresas indesejadas após a implementação.
Ao adotar uma abordagem metódica, baseada em dados e adaptada às especificidades de cada aplicação e conjunto de dados, os desenvolvedores podem maximizar os benefícios do multi-threading, transformando-o em um aliado valioso na otimização contínua da performance de aplicativos.
Newsletter
Gostou desse conteúdo? Aproveita pra se inscrever na nossa newsletter https://highperformanceletterbr.substack.com/
Postamos conteúdos sobre performance e otimização todas as semanas!
Primeiro curso de performance backend js do brasil(ou do mundo)
🚀 Domine o Desempenho Backend com JavaScript! 🚀
🎓 Apresentamos o PRIMEIRO Curso de Performance Backend em JavaScript no Brasil! 🇧🇷
🔥 Seja um dos pioneiros a explorar o mundo do desenvolvimento backend com foco em eficiência e velocidade. Este curso exclusivo é sua porta de entrada para se tornar um especialista em otimização e desempenho de aplicações JavaScript.
🌟 O Que Oferecemos:
Instrutores Especialista: Aprenda com um profissional que atua com esse assunto no dia a dia.
Conteúdo Avançado: Desde fundamentos até técnicas avançadas de otimização.
Projetos Práticos: Coloque suas habilidades à prova com desafios reais.
Comunidade e Networking: Junte-se a uma comunidade vibrante de desenvolvedores.
🛠️ Domine Ferramentas e Técnicas Essenciais: Conteúdo prático sobre as melhores tools de monitoramento e análise de recursos
Análise de Performance;
Diagnóstico;
Otimização de Código;
Gerenciamento Eficiente de Memória;
Eventos e streaming;
🚀 Transforme a forma que os outros devs te veem:
Esteja na vanguarda do desenvolvimento backend. As habilidades que você adquirirá são inestimáveis e altamente procuradas no mercado de trabalho para posições senior.
Top comments (0)