Tudo em Como o Node.js Funciona e O Event Loop do Node.js descreve uma única thread JavaScript dentro de um único processo do SO.
Esse modelo é rápido para I/O, mas não tem resposta para trabalho intensivo em CPU, para executar outro programa ou para usar mais de um núcleo de CPU.
Esta página é sobre as três maneiras que o Node permite que você saia dessa única thread - child_process, worker_threads e cluster - e o modelo mental compartilhado que torna suas diferenças previsíveis em vez de arbitrárias.
Fundamentos de Processos percorre código funcional para cada um deles; child_process, worker_threads e Módulo cluster aprofundam a sintaxe e os padrões de produção para cada um individualmente.
Aqui, o objetivo é o mapa: o que realmente muda - memória, custo de inicialização, isolamento de falhas - à medida que você passa de permanecer no processo para iniciar um novo processo completo.
As válvulas de escape de concorrência do Node ficam em um espectro de "compartilhar tudo" (permanecer no processo) a "compartilhar nada" (um processo de SO separado), e cada opção troca isolamento por custo de uma maneira específica e previsível.
Por que Importa: Usar o primitivo errado - um novo processo para uma tarefa que só precisava de uma thread, ou um pool de threads para uma tarefa que precisava de isolamento de falhas em nível de processo - desperdiça recursos ou permite que uma falha cause mais danos do que deveria.
Conceitos Chave:Processo de SO, thread, isoleto V8, structured clone, memória compartilhada, canal IPC.
Quando Usar Este Modelo: Decidir se o trabalho intensivo em CPU pertence a uma thread worker, se uma tarefa precisa de um processo filho completo, se cluster é a maneira correta de usar vários núcleos e ler as páginas por primitivo que seguem com a moldura certa já em vigor.
Limitações / Trade-offs: Nenhum desses primitivos é gratuito - processos custam memória e tempo de inicialização, threads custam serialização em cada mensagem, e ambos adicionam superfície operacional real (mais coisas que podem falhar, mais coisas para monitorar).
Tópicos Relacionados: o event loop, child_process, worker_threads, o módulo cluster, graceful shutdown.
Um processo de SO é a unidade de isolamento do sistema operacional: seu próprio espaço de memória, seus próprios descritores de arquivo, seu próprio domínio de falha.
Quando um processo falha, o sistema operacional garante que ele não pode corromper a memória de outro processo - essa garantia é exatamente o que você compra ao iniciar um.
Uma thread, por outro lado, é uma unidade de execução que roda dentro de um processo e, na maioria das linguagens, compartilha a memória desse processo diretamente com todas as outras threads nele.
Os worker_threads do Node dobram essa segunda definição ligeiramente: cada worker recebe seu próprio isoleto V8 - seu próprio heap JavaScript, seu próprio coletor de lixo, seu próprio event loop - mesmo que todos vivam dentro de um único processo de SO.
É por isso que os workers se sentem mais próximos de "processos" em comportamento (estado JS isolado, passagem de mensagens em vez de variáveis compartilhadas), enquanto permanecem mais baratos do que um verdadeiro processo de SO para iniciar.
Compartilhar memória entre workers é possível, mas apenas deliberadamente, através de um SharedArrayBuffer e Atomics - o padrão é copiar, não compartilhar.
Uma analogia útil: permanecer no processo é um único atendente lidando com todas as solicitações em uma única mesa.
Iniciar um child_process é abrir um segundo escritório totalmente independente na cidade - nada é compartilhado, e se ele queimar, seu escritório não será afetado, mas toda tarefa lá custa uma viagem.
Um worker worker_threads é mais como trazer um segundo atendente para o mesmo prédio, em uma sala separada - eles podem passar bilhetes por baixo da porta (mensagens), e só verão a mesa um do outro se você explicitamente configurar um quadro branco compartilhado (SharedArrayBuffer).
// Mesma tarefa, três níveis de isolamento diferentesnew Worker('./task.js', { workerData }); // processo compartilhado, isoleto V8 separadospawn('some-binary', ['--flag']); // processo de SO separado, sem memória compartilhadacluster.fork(); // processo de SO separado, socket de escuta compartilhado
child_process inicia um processo de SO genuinamente separado - ele pode executar qualquer executável, não apenas scripts Node, e não compartilha nada com o pai por padrão.
A comunicação ocorre através de pipes stdio, ou através de um canal IPC dedicado quando você usa fork() especificamente (que inicia um filho Node e configura process.send()/message eventos para você).
Esta é a opção mais pesada em custo de inicialização e memória, e também é o isolamento mais forte: uma falha no processo filho não pode corromper a memória do pai.
worker_threads cria uma nova thread dentro do mesmo processo, com seu próprio heap V8 isolado, mas recursos em nível de SO compartilhados, como descritores de arquivo.
A inicialização é significativamente mais barata do que iniciar um processo, e workerData clona uma carga útil de inicialização uma vez; a comunicação contínua através de postMessage usa o algoritmo structured clone, que copia dados em vez de compartilhá-los.
Uma exceção não capturada dentro de um worker termina especificamente esse worker - a thread principal e quaisquer outros workers continuam rodando, desde que algo esteja ouvindo o evento 'error' do worker para observar o que aconteceu.
cluster não é um quarto primitivo independente - mecanicamente, é child_process.fork() por baixo dos panos, envolvido com lógica que permite que vários processos Node "forkados" compartilhem um único socket de escuta.
O sistema operacional (ou a própria lógica round-robin do Node, dependendo da plataforma) distribui as conexões de entrada entre esses processos, que é como um runtime de thread única usa mais de um núcleo de CPU para um servidor HTTP.
Como cada worker do cluster é um processo separado completo, nenhum deles compartilha estado em memória - um cache aquecido em um worker é invisível para os outros, o que é uma fonte comum de confusão para qualquer pessoa que assuma que cluster se comporta como um pool de threads.
A consequência prática de tudo isso: worker_threads é para JavaScript intensivo em CPU que precisa sair da thread principal sem o overhead de inicialização de processo; child_process é para executar outros programas ou para isolamento forte o suficiente para que uma falha realmente não toque no resto; cluster é para usar múltiplos núcleos para servir um tipo de trabalho voltado para a rede, ao custo de memória duplicada e sem cache compartilhado.
O isolamento de falhas é o eixo que a maioria das equipes subestima até que um incidente force a questão.
Uma falha de thread worker é contida nesse worker, mas ele ainda compartilha o processo de SO com todo o resto - uma falha grave de código nativo dentro de um worker (um segfault em um addon nativo, por exemplo) ainda pode derrubar todo o processo, incluindo os workers.
Uma falha de processo filho é ainda mais contida: a fronteira do processo do sistema operacional é uma garantia muito mais forte do que a fronteira do isoleto V8.
Escalar com cluster é um padrão legítimo em um único host multi-core, mas compete com uma resposta diferente e cada vez mais comum para o mesmo problema: escalar réplicas horizontalmente sob um orquestrador como Kubernetes.
Ambas as abordagens resolvem "usar mais de um núcleo", mas resolvem em camadas diferentes - cluster dentro de uma implantação Node, orquestração entre muitas implantações de thread única - e executar ambos ao mesmo tempo geralmente apenas adiciona complexidade sem adicionar capacidade.
Considerações de segurança e observabilidade diferem acentuadamente por primitivo.
child_process com shell: true e entrada não sanitizada é um vetor direto de injeção de comando - passar um array de argumentos para spawn em vez de uma string de shell evita essa classe de bug completamente.
Cada primitivo aqui também multiplica sua superfície de monitoramento: um PID por processo filho, um threadId por worker, e rastreamento de CPU/memória por worker são todos necessários quando você não está mais olhando para um único event loop.
O ecossistema convergiu em pools de workers como a unidade prática de adoção para trabalho intensivo em CPU, em vez de iniciar workers ad hoc por solicitação - bibliotecas como piscina gerenciam um pool fixo, enfileiramento e ciclo de vida para que o código do aplicativo não reimplemente o agendamento.
Abordagem
Força
Fraqueza
Melhor Encaixe
Permanecer no processo (async/await)
Overhead zero, modelo mental mais simples
Nenhuma ajuda para trabalho intensivo em CPU
Manipuladores I/O-bound, o padrão esmagador
worker_threads
Barato para iniciar, falhas isoladas, memória compartilhada opcional
Custo de serialização por mensagem, a menos que use SharedArrayBuffer
JS intensivo em CPU: hashing, processamento de imagem, transformações de dados grandes
child_process
Isolamento mais forte, pode executar binários não-Node
Maior custo de inicialização, sem memória compartilhada
Executar ferramentas externas, isolar trabalho genuinamente não confiável ou frágil
cluster
Usa múltiplos núcleos para um listener HTTP
Memória duplicada por worker, sem cache compartilhado na memória
Servidor único multi-core servindo um serviço voltado para HTTP
"Node é single-threaded, então não pode usar múltiplos núcleos de CPU." A thread JavaScript principal é single-threaded, mas worker_threads, child_process e cluster existem especificamente para usar mais de um núcleo quando o trabalho justifica isso.
"worker_threads e child_process são a mesma coisa com nomes diferentes." Eles diferem em nível de isolamento e custo - workers compartilham um processo e copiam mensagens por padrão; processos filhos são totalmente separados, sem memória compartilhada alguma.
"cluster dá aos workers memória compartilhada." Workers de cluster são processos de SO separados; nada é compartilhado automaticamente, e qualquer estado que um worker precise em comum com seus irmãos tem que viver em algum lugar externo, como Redis ou um banco de dados.
"Iniciar um worker por solicitação é como paralelizar trabalho de CPU." Isso recria o exato problema de overhead que os workers se destinam a evitar - código de produção usa um pool de tamanho fixo, reutilizado entre solicitações.
"Um worker ou processo filho que falha sempre derruba todo o aplicativo." Isolamento é o ponto - uma exceção não capturada de um worker termina esse worker, e a falha de um processo filho permanece dentro da fronteira do processo do SO, desde que algo esteja observando o evento de falha.
Qual é a maneira mais simples de decidir entre permanecer no processo, um thread worker e um processo filho?
Pergunte primeiro que tipo de trabalho é: I/O puro permanece no processo, JavaScript intensivo em CPU vai para um pool worker_threads, e executar outro programa (ou precisar de isolamento de nível de processo rígido) vai para child_process.
O `cluster` é um mecanismo subjacente diferente do `child_process`?
Não - cluster é construído sobre child_process.fork(), com lógica adicional para compartilhar um socket de escuta entre os processos "forkados" para que um servidor HTTP possa usar múltiplos núcleos de CPU.
Os threads workers compartilham memória com a thread principal por padrão?
Não - postMessage e workerData copiam dados usando o algoritmo structured clone; o compartilhamento de memória real requer o uso explícito de um SharedArrayBuffer com Atomics.
Por que iniciar um worker `worker_threads` é mais barato do que iniciar um `child_process`?
Um worker permanece dentro do mesmo processo de SO e apenas cria um novo isoleto V8 e thread, enquanto um processo filho pede ao sistema operacional um processo totalmente novo com seu próprio espaço de memória e descritores de arquivo - uma operação muito mais pesada.
Uma falha em um worker de cluster pode derrubar todo o servidor?
Não por padrão - cada worker de cluster é um processo de SO separado, então um que falha não corrompe os outros, embora o processo principal deva ser escrito para detectar a saída e "forkar" um substituto.
O que acontece se um thread worker lançar uma exceção não capturada?
Esse worker específico termina; a thread principal e quaisquer workers irmãos continuam rodando, desde que o código que criou o worker esteja ouvindo seu evento 'error'.
Quando o `cluster` é a ferramenta errada, mesmo que ele "use mais núcleos"?
Quando você já está executando várias réplicas sob um orquestrador como Kubernetes - combinar escalonamento horizontal em nível de pod com forking cluster no processo geralmente adiciona complexidade operacional sem adicionar capacidade real.
Por que `spawn('cmd', args)` importa mais do que `exec('cmd ' + args)` para segurança?
spawn com um array de argumentos passa argumentos diretamente para o SO sem passar por um shell, então a entrada controlada pelo usuário em um argumento não pode ser interpretada como sintaxe de shell - exec (ou spawn com shell: true) abre essa porta.
Preciso de uma biblioteca de pool de workers ou posso gerenciar os workers eu mesmo?
Você pode gerenciar um pequeno pool fixo sozinho, mas bibliotecas como piscina lidam corretamente com enfileiramento, backpressure e ciclo de vida - reimplementar isso do zero é fácil de obter resultados sutilmente errados sob carga.
O `worker_threads` ajuda se meu gargalo for realmente I/O, não CPU?
Não - trabalho I/O-bound (chamadas de rede, a maioria das consultas de banco de dados) já cede o event loop via await e não obtém benefício de uma thread separada; workers só ajudam quando o trabalho é genuinamente JavaScript intensivo em CPU.
Qual é a diferença entre `child_process.exec` e `child_process.fork`?
exec executa um comando de shell arbitrário e armazena sua saída em buffer; fork especificamente inicia um novo processo Node.js executando um script fornecido e configura um canal IPC para passagem de mensagens estruturadas entre pai e filho.
Existe uma maneira de compartilhar um cache real em memória entre workers de cluster?
Não diretamente - como workers de cluster são processos separados, um cache compartilhado tem que viver fora de qualquer worker individual, tipicamente em Redis ou outra loja externa que todos os workers possam ler e escrever.