Node.js: Worker Threads
Última atualização: 2026-08-26
1. O que você vai aprender
- Use a classe
Workerpara criar threads de trabalho e gerenciar seus ciclos de vida - Implementação da comunicação bidirecional de mensagens entre o thread principal e os workers usando
parentPort - Use
workerDatapara passar os dados de inicialização ao Worker - Use
MessageChannel/MessagePortpara estabelecer comunicação direta entre threads - Executar operações de memória compartilhada usando
SharedArrayBuffereAtomics - Implementar um padrão de pool de threads para reutilizar os workers e melhorar a taxa de processamento
- Comparando os casos de uso adequados para threads de trabalho,
child_processecluster
2. Matéria: A aceleração do processamento de imagens da Alice
Alice é responsável pelos serviços de processamento de imagens em uma empresa de SaaS. Quando os usuários enviam imagens, o sistema precisa gerar miniaturas em três tamanhos diferentes para cada imagem. A solução original, que funcionava em um único thread, levava cerca de 30 segundos para processar 100 imagens e, durante os horários de pico, havia um acúmulo significativo de solicitações.
Ela decidiu usar threads de trabalho para distribuir as tarefas entre quatro threads para processamento paralelo: o thread principal lê a lista de arquivos e passa os parâmetros da tarefa por meio de workerData, e os threads de trabalho retornam os resultados por meio de parentPort após gerarem as miniaturas. No final, o tempo de processamento para 100 imagens foi reduzido para cerca de 8 segundos, e a taxa de processamento aumentou quase quatro vezes.
3. Noções básicas sobre a classe trabalhadora
O módulo worker_threads oferece recursos reais de multithreading no Node.js. Cada Worker executa scripts em um thread separado e possui sua própria instância do mecanismo V8 e seu próprio loop de eventos.
const { Worker } = require('worker_threads');
const worker = new Worker('./heavy-task.js', {
workerData: { taskId: 42, input: 'hello' }
});
worker.on('message', (result) => {
console.log('Result from worker:', result);
});
worker.on('error', (err) => {
console.error('Worker error:', err);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker stopped with exit code ${code}`);
}
});
Dentro do script Worker:
// heavy-task.js
const { parentPort, workerData } = require('worker_threads');
const { taskId, input } = workerData;
const result = performHeavyComputation(input);
parentPort.postMessage({ taskId, result });
function performHeavyComputation(data) {
let hash = 0;
for (let i = 0; i < 1e8; i++) {
hash = (hash + data.charCodeAt(i % data.length)) % 65536;
}
return hash;
}
▶ Exemplo: Criando e se comunicando com um “worker” básico
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.postMessage('Hello from worker!');
`, { eval: true });
worker.on('message', (msg) => {
console.log(msg); // Hello from worker!
});
4. Comunicação bidirecional do parentPort
parentPort é o canal de mensagens entre o Worker e o thread principal. O thread principal envia mensagens por meio de worker.postMessage(), e o Worker responde por meio de parentPort.postMessage(). Ambos os lados ficam atentos aos eventos message para receber dados.
▶ Exemplo: Troca de mensagens bidirecional
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./echo-worker.js');
worker.postMessage({ action: 'greet', name: 'Alice' });
worker.on('message', (msg) => {
console.log('Main received:', msg);
if (msg.action === 'greet_reply') {
worker.postMessage({ action: 'task', payload: 100 });
}
});
// echo-worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (msg) => {
if (msg.action === 'greet') {
parentPort.postMessage({
action: 'greet_reply',
text: `Hello, ${msg.name}!`
});
} else if (msg.action === 'task') {
const result = msg.payload * 2;
parentPort.postMessage({ action: 'result', value: result });
}
});
▶ Exemplo: Transferência sem cópia de objetos transferíveis
const { Worker } = require('worker_threads');
const buffer = new ArrayBuffer(1024 * 1024); // 1 MB
const worker = new Worker('./process-buffer.js');
worker.postMessage({ buffer }, [buffer]);
console.log('Buffer transferred, main thread no longer owns it');
// process-buffer.js
const { parentPort, workerData } = require('worker_threads');
parentPort.on('message', ({ buffer }) => {
const view = new Uint8Array(buffer);
view[0] = 42;
parentPort.postMessage({ done: true, firstByte: view[0] }, [buffer]);
});
5. Dados de inicialização do workerData
workerData são dados iniciais somente para leitura, passados por meio de opções ao criar um Worker. O Worker lê esses dados diretamente internamente, sem a necessidade de troca de mensagens. É adequado para passar configurações, caminhos de arquivos, parâmetros de tarefas e assim por diante.
▶ Exemplo: Trabalhador de processamento em lote de imagens
// main.js
const { Worker } = require('worker_threads');
const path = require('path');
const imageFiles = [
'photo-001.jpg', 'photo-002.jpg', 'photo-003.jpg',
'photo-004.jpg', 'photo-005.jpg', 'photo-006.jpg'
];
const WORKER_COUNT = 4;
const chunkSize = Math.ceil(imageFiles.length / WORKER_COUNT);
for (let i = 0; i < WORKER_COUNT; i++) {
const chunk = imageFiles.slice(i * chunkSize, (i + 1) * chunkSize);
const worker = new Worker('./image-worker.js', {
workerData: {
workerId: i,
files: chunk,
outputDir: './thumbnails'
}
});
worker.on('message', (result) => {
console.log(`Worker ${i} done:`, result.processed);
});
}
// image-worker.js
const { parentPort, workerData } = require('worker_threads');
const path = require('path');
const { workerId, files, outputDir } = workerData;
async function generateThumbnail(file) {
// Simulate image processing
return new Promise((resolve) => {
setTimeout(() => resolve(`${file} -> thumb`), 100);
});
}
(async () => {
const processed = [];
for (const file of files) {
const result = await generateThumbnail(file);
processed.push(result);
}
parentPort.postMessage({ workerId, processed });
})();
6. MessageChannel e MessagePort
MessageChannel Crie um par de instâncias MessagePort interconectadas que possam ser atribuídas a diferentes trabalhadores, permitindo a comunicação direta entre threads sem passar pelo thread principal.
graph LR
Main["Main Thread"] -->|"worker.postMessage()"| W1["Worker 1"]
W1 -->|"parentPort.postMessage()"| Main
Main -->|"worker.postMessage()"| W2["Worker 2"]
W2 -->|"parentPort.postMessage()"| Main
W1 <-->|"MessagePort"| W2
▶ Exemplo: Dois trabalhadores se comunicando diretamente
// main.js
const { Worker, MessageChannel } = require('worker_threads');
const worker1 = new Worker('./chat-worker.js', {
workerData: { id: 1 }
});
const worker2 = new Worker('./chat-worker.js', {
workerData: { id: 2 }
});
const { port1, port2 } = new MessageChannel();
worker1.postMessage({ port: port1 }, [port1]);
worker2.postMessage({ port: port2 }, [port2]);
// chat-worker.js
const { parentPort, workerData } = require('worker_threads');
parentPort.once('message', ({ port }) => {
port.on('message', (msg) => {
console.log(`Worker ${workerData.id} received:`, msg);
});
setInterval(() => {
port.postMessage(`Hello from Worker ${workerData.id}`);
}, 1000);
});
7. SharedArrayBuffer e operações atômicas
SharedArrayBuffer Permite que múltiplas threads compartilhem o mesmo bloco de memória. Quando usado em conjunto com as operações atômicas fornecidas por Atomics, possibilita a leitura e a gravação seguras de dados compartilhados, evitando assim condições de corrida.
▶ Exemplo: Contador compartilhado
// main.js
const { Worker } = require('worker_threads');
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
const WORKER_COUNT = 4;
const INCREMENTS_PER_WORKER = 100000;
for (let i = 0; i < WORKER_COUNT; i++) {
const worker = new Worker('./counter-worker.js', {
workerData: { sharedBuffer, increments: INCREMENTS_PER_WORKER }
});
worker.on('exit', () => {
const final = Atomics.load(sharedArray, 0);
console.log(`Final counter value: ${final}`);
});
}
// counter-worker.js
const { parentPort, workerData } = require('worker_threads');
const { sharedBuffer, increments } = workerData;
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < increments; i++) {
Atomics.add(sharedArray, 0, 1);
}
parentPort.postMessage('done');
8. Comparação dos métodos de comunicação
| Método de comunicação | Direção | Características | Cenários de aplicação |
|---|---|---|---|
parentPort |
Thread principal ↔ Worker | Troca bidirecional de mensagens, clonagem estruturada | Comunicação geral entre tarefas |
workerData |
Thread principal → Worker | Somente leitura; passado na criação | Configuração/parâmetros de inicialização |
MessageChannel |
Trabalhador ↔ Trabalhador | Conexão direta entre portas, contornando a thread principal | Colaboração entre threads |
SharedArrayBuffer |
Compartilhado por todos os threads | Sem cópia, requer Atomics | Troca de dados em alta frequência |
9. Worker x child_process x cluster
| Recurso | Tarefas | child_process | cluster |
|---|---|---|---|
| Unidade | Threads | Processos | Processos |
| Memória | Memória compartilhada do processo | Espaço de memória independente | Espaço de memória independente |
| Comunicação | Mensagens / Memória compartilhada | Serialização de IPC | Serialização de IPC |
| Custos iniciais | Moderados | Altos | Altos |
| Aplicável | Computação com uso intensivo da CPU | Programas independentes/sandboxes | Serviços HTTP multicore |
| Estabilidade | Falhas nos processos de trabalho afetam o próprio processo | Falhas nos processos filhos não afetam uns aos outros | Falhas nos processos de trabalho não afetam uns aos outros |
| Status compartilhado | SharedArrayBuffer | Não suportado | Não suportado |
10. Tarefas adequadas e inadequadas para multithreading
| Adequado para multithreading | Não adequado para multithreading |
|---|---|
| Processamento de imagens/Geração de miniaturas | Roteamento simples de solicitações HTTP |
| Cálculos de criptografia/hash | Operações CRUD em bancos de dados |
| Compressão/Descompressão | E/S de arquivos (assíncrona é suficiente) |
| Operações matemáticas em grande escala | Conversões simples de JSON |
| Geração/Renderização de PDF | Microtarefas de curta duração |
| Transcodificação de áudio e vídeo | Encaminhamento de mensagens orientado por eventos |
11. Padrão de pool de threads
A criação de um Worker acarreta alguma sobrecarga, e criá-los e destruí-los com frequência desperdiça recursos. Um pool de threads mantém um número fixo de Workers; quando uma tarefa chega, ela é atribuída a um Worker ocioso e, assim que a tarefa é concluída, o Worker é recuperado e reutilizado.
▶ Exemplo: Criando um pool de threads simples do zero
// pool.js
const { Worker } = require('worker_threads');
class Pool {
constructor(workerFile, size) {
this.workerFile = workerFile;
this.size = size;
this.workers = [];
this.queue = [];
for (let i = 0; i < size; i++) {
const worker = new Worker(workerFile);
worker.busy = false;
worker.on('message', (result) => {
const task = worker.currentTask;
worker.busy = false;
worker.currentTask = null;
task.resolve(result);
this._processQueue();
});
worker.on('error', (err) => {
const task = worker.currentTask;
if (task) {
worker.busy = false;
worker.currentTask = null;
task.reject(err);
this._processQueue();
}
});
this.workers.push(worker);
}
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const idle = this.workers.find((w) => !w.busy);
if (idle) {
this._assign(idle, task);
} else {
this.queue.push(task);
}
});
}
_assign(worker, task) {
worker.busy = true;
worker.currentTask = task;
worker.postMessage(task.data);
}
_processQueue() {
if (this.queue.length === 0) return;
const idle = this.workers.find((w) => !w.busy);
if (!idle) return;
const task = this.queue.shift();
this._assign(idle, task);
}
destroy() {
for (const worker of this.workers) {
worker.terminate();
}
this.workers = [];
this.queue = [];
}
}
module.exports = Pool;
▶ Exemplo: Cálculo da sequência de Fibonacci usando um pool de threads
// main.js
const Pool = require('./pool.js');
const pool = new Pool('./fib-worker.js', 4);
async function main() {
const tasks = [40, 41, 42, 43, 44, 45, 46, 47];
const promises = tasks.map((n) => pool.run({ n }));
const results = await Promise.all(promises);
for (let i = 0; i < tasks.length; i++) {
console.log(`fib(${tasks[i]}) = ${results[i]}`);
}
pool.destroy();
}
main();
// fib-worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', ({ n }) => {
const result = fib(n);
parentPort.postMessage(result);
});
function fib(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
▶ Exemplo: biblioteca de pool de threads “piscina”
npm install piscina
const path = require('path');
const Piscina = require('piscina');
const pool = new Piscina({
filename: path.resolve(__dirname, 'task.js'),
maxThreads: 4
});
async function main() {
const results = await Promise.all([
pool.run({ x: 10, y: 20 }),
pool.run({ x: 30, y: 40 }),
pool.run({ x: 50, y: 60 })
]);
console.log(results); // [30, 70, 110]
await pool.destroy();
}
main();
// task.js
module.exports = ({ x, y }) => {
return x + y;
};
12. Exemplo abrangente: Pool de threads para processamento de imagens
Utilizando os conceitos abordados anteriormente, crie um sistema completo de pool de threads para processamento de imagens: a thread principal distribui as tarefas, as threads de trabalho processam as miniaturas e os resultados são coletados e agregados.
// image-pool.js
const { Worker } = require('worker_threads');
const path = require('path');
class ImagePool {
constructor(workerCount) {
this.workers = [];
this.queue = [];
this.results = [];
for (let i = 0; i < workerCount; i++) {
const worker = new Worker(path.join(__dirname, 'image-processor.js'));
worker.busy = false;
worker.on('message', (msg) => {
if (msg.type === 'result') {
this.results.push(msg.data);
}
worker.busy = false;
worker.currentResolve();
this._dispatch();
});
worker.on('error', (err) => {
worker.busy = false;
if (worker.currentReject) {
worker.currentReject(err);
}
this._dispatch();
});
this.workers.push(worker);
}
}
process(fileList) {
this.results = [];
const promises = fileList.map((file) => this._enqueue(file));
return Promise.all(promises).then(() => this.results);
}
_enqueue(file) {
return new Promise((resolve, reject) => {
this.queue.push({ file, resolve, reject });
this._dispatch();
});
}
_dispatch() {
while (this.queue.length > 0) {
const idle = this.workers.find((w) => !w.busy);
if (!idle) break;
const task = this.queue.shift();
idle.busy = true;
idle.currentResolve = task.resolve;
idle.currentReject = task.reject;
idle.postMessage({ file: task.file, sizes: [200, 400, 800] });
}
}
destroy() {
this.workers.forEach((w) => w.terminate());
}
}
module.exports = ImagePool;
// image-processor.js
const { parentPort } = require('worker_threads');
parentPort.on('message', async ({ file, sizes }) => {
const results = [];
for (const size of sizes) {
const thumb = await resize(file, size);
results.push(thumb);
}
parentPort.postMessage({
type: 'result',
data: { file, thumbnails: results }
});
});
async function resize(file, maxSize) {
return new Promise((resolve) => {
const duration = Math.random() * 200 + 50;
setTimeout(() => {
resolve(`${file}_${maxSize}px.jpg`);
}, duration);
});
}
// run.js
const ImagePool = require('./image-pool.js');
async function main() {
const pool = new ImagePool(4);
const files = Array.from({ length: 20 }, (_, i) =>
`photo-${String(i + 1).padStart(3, '0')}.jpg`
);
const start = Date.now();
const results = await pool.process(files);
const elapsed = Date.now() - start;
console.log(`Processed ${files.length} images in ${elapsed} ms`);
console.log(`Generated ${results.reduce((sum, r) => sum + r.thumbnails.length, 0)} thumbnails`);
pool.destroy();
}
main();
node run.js
Processed 20 images in 1234 ms
Generated 60 thumbnails
❓ Perguntas Frequentes
P: Qual é a diferença entre threads de trabalho e
child_process? R: As threads de trabalho são criadas dentro do mesmo processo e podem compartilhar memória (SharedArrayBuffer), resultando em baixa sobrecarga de comunicação;child_processcria processos separados com memória isolada e se comunica por meio de IPC serializado, o que apresenta maior sobrecarga, mas é mais seguro.
P: Um worker pode acessar variáveis na thread principal? R: Não. Os workers têm seu próprio contexto de execução e não podem acessar diretamente as variáveis da thread principal. É necessário usar
postMessagepara a troca de mensagens, passar valores por meio deworkerDatadurante a inicialização ou usarSharedArrayBufferpara compartilhar memória.
P: Quando se deve usar threads de trabalho? R: Para tarefas que exigem muito da CPU, como processamento de imagens, criptografia, compactação e descompactação, cálculos matemáticos em grande escala e transcodificação de áudio e vídeo. Tarefas que exigem muito da E/S não requerem threads de trabalho; a E/S assíncrona do Node.js já é suficientemente eficiente.
P: O SharedArrayBuffer é seguro? R: O SharedArrayBuffer não oferece, por si só, nenhum mecanismo de sincronização; leituras e gravações simultâneas em um ambiente multithread podem levar a condições de corrida. É necessário usar operações atômicas (como add, load, store e compareExchange) para garantir a atomicidade ou utilizar bloqueios para coordenar o acesso.
P: Existe uma sobrecarga significativa na criação de um Worker? R: A criação de um Worker requer a inicialização de uma instância do motor V8 e de um ciclo de eventos, o que leva cerca de 30 a 50 ms — menos do que com
child_process, mas ainda assim não é algo desprezível. Recomendamos reutilizar os Workers em um pool de threads para evitar criações e destruições frequentes.
P: Os módulos do Node.js, como
fsehttp, podem ser usados dentro de um Worker? R: Sim. As threads do Worker possuem um ambiente de execução completo do Node.js e oferecem suporte à grande maioria dos módulos integrados. No entanto, certos módulos, comocluster, só podem ser usados no processo principal.
P: Qual é a diferença entre “transferível” e “clonagem estruturada”? R: A clonagem estruturada copia os dados e é adequada para pequenos conjuntos de dados; a opção “transferível” transfere a propriedade para a thread de destino — trata-se de uma operação sem cópia, mas a thread original perde o acesso — e é adequada para cenários que envolvem ArrayBuffers grandes e similares.
📖 Resumo
- Conceitos-chave e como aplicá-los
- Artigo: Conceitos fundamentais e uso do Alice para acelerar o processamento de imagens
- Conceitos básicos e uso da classe Worker
- parentPort: Conceitos básicos e uso da comunicação bidirecional
- Conceitos básicos e uso dos dados de inicialização do
workerData - Conceitos básicos e uso do MessageChannel e do MessagePort
- Conceitos básicos e uso do SharedArrayBuffer e das funções atômicas
- Conceitos-chave e uso das comparações entre métodos de comunicação
📝 Exercícios
- Conclua todos os exemplos de código desta lição e certifique-se de que cada um deles seja executado corretamente.
- Modifique o exemplo completo e adicione suas próprias extensões
- Analise a documentação oficial, identifique 1 ou 2 APIs que não foram abordadas nesta aula e escreva um código de teste para elas.
- Reflexão: Como você aplicaria o que aprendeu nesta aula a um projeto do mundo real?
- Tente combinar o que você aprendeu nesta aula com o conteúdo das aulas anteriores para criar um pequeno projeto.