
Todos nós sabemos que o Node.js usa um modelo de E/S assíncrono orientado a eventos e de thread único. Suas características determinam que ele não pode aproveitar as vantagens da CPU multi-core e não é bom para concluir algumas operações não E/S (). como execução de scripts), computação de IA, processamento de imagens, etc.), para resolver tais problemas, o Node.js fornece uma solução convencional de multiprocessos (thread) (para discussões sobre processos e threads, consulte o autor do outro artigo Node.js e modelo de simultaneidade), este artigo apresentará o mecanismo multithread do Node.js.
Podemos usar o módulo child_process para criar um processo filho do Node.js para concluir algumas tarefas especiais (como a execução de scripts). Este módulo fornece principalmente exec , execFile , fork , spwan e outros métodos. . usar.
const { exec } = require('child_process');
exec('ls -al', (erro, stdout, stderr) => {
console.log(stdout);
}); Este método processa a string de comando de acordo com o arquivo executável especificado por options.shell , armazena em cache sua saída durante a execução do comando e, em seguida, retorna o resultado da execução na forma de parâmetros de função de retorno de chamada até que a execução do comando seja concluída.
Os parâmetros deste método são explicados a seguir:
command : o comando a ser executado (como ls -al
options : configurações de parâmetros (opcional), as propriedades relevantes são as seguintes:
cwd : o diretório de trabalho atual do processo filho , o valor padrão é process.cwd() ;
env : configuração da variável de ambiente (objeto de par chave-valor), o valor padrão é o valor de process.env
: encoding de caracteres, o valor padrão é: utf8
shell : executável; arquivo que processa strings de comando, o valor padrão no Unix é /bin/sh , o valor padrão no Windows é o valor de process.env.ComSpec (se estiver vazio, é cmd.exe por exemplo:
const { exec ); } = require('processo_filho');
exec("print('Olá mundo!')", { shell: 'python' }, (erro, stdout, stderr) => {
console.log(stdout);
}); Executar o exemplo acima gerará Hello World! que é equivalente ao subprocesso que executa python -c "print('Hello World!')" . arquivo executável especificado. A execução de instruções relacionadas por meio da opção -c deve ser suportada.
Nota: Acontece que Node.js também suporta a opção -c , mas é equivalente à opção --check . Ela é usada apenas para detectar se há erros de sintaxe no script especificado e não executará o script relevante.
signal : Use o AbortSignal especificado para encerrar o processo filho. Este atributo está disponível acima da versão 14.17.0, por exemplo:
const { exec } = require('child_process');
const ac = new AbortController();
exec('ls -al', { signal: ac.signal }, (error, stdout, stderr) => {}); No exemplo acima, podemos encerrar o processo filho antecipadamente chamando ac.abort() .
timeout : O tempo limite do processo filho (se o valor deste atributo for maior que 0 , então quando o tempo de execução do processo filho exceder o valor especificado, o sinal de encerramento especificado pelo atributo killSignal será enviado ao processo filho ), em milímetros, o valor padrão é 0 ;
maxBuffer : o cache máximo (binário) permitido por 1024 * 1024 ou stderr. Se excedido, o processo filho será eliminado e qualquer saída será truncada
killSignal : O sinal de encerramento do processo filho, o valor padrão é SIGTERM :
uid para executar o processo filho uid
gid : gid para executar o processo filho
windowsHide : se deseja ocultar a janela do console do processo filho, comumente usado em sistemas Windows ; o valor padrão é false
: função de retorno de callback , incluindo error , stdout , stderr Parâmetros:
error : Se a linha de comando for executada com sucesso, o valor é null , caso contrário, o valor é uma instância de Error, onde error.code é a saída. código de erro do processo filho, error.signal é o sinal para o encerramento do processo filho;stdout e stderr : filho stdout e stderr do processo são codificados de acordo com o valor do atributo encoding Se encoding for buffer . , ou o valor de stdout ou stderr for uma string irreconhecível, ela será codificada de acordo com buffer .const { execFile } = require('child_process');
execFile('ls', ['-al'], (erro, stdout, stderr) => {
console.log(stdout);
}); A função deste método é semelhante a exec . A única diferença é que execFile processa diretamente o comando com o arquivo executável especificado (ou seja, o valor do parâmetro file ) por padrão, o que torna sua eficiência um pouco maior que exec (se você olhar para a lógica de processamento do shell, sinto que a eficiência é insignificante).
Os parâmetros deste método são explicados a seguir:
file : o nome ou caminho do arquivo executável;
args : a lista de parâmetros do arquivo executável
: options de parâmetros (não podem ser especificadas), as propriedades relevantes são as seguintes:
shell : quando o valor é false significa usar diretamente o arquivo executável especificado (ou seja, o valor do file de parâmetro) processa o comando. Quando o valor é true ou outras strings, a função é equivalente shell em exec . O valor padrão é false ;killSignal maxBuffer uid gid env timeout encoding cwdwindowsVerbatimArguments Windows Unix é false ;windowsHide e signal foram introduzidos acima e não serão repetidos aqui.callback : função de retorno de chamada, que equivale ao callback em exec e não será explicada aqui.
const { fork } = require('child_process');
const echo = fork('./echo.js', {
silencioso: verdadeiro
});
echo.stdout.on('dados', (dados) => {
console.log(`stdout: ${dados}`);
});
echo.stderr.on('dados', (dados) => {
console.error(`stderr: ${dados}`);
});
echo.on('fechar', (código) => {
console.log(`processo filho encerrado com código ${code}`);
}); Este método é usado para criar uma nova instância do Node.js para executar o script Node.js especificado e se comunicar com o processo pai por meio do IPC.
Os parâmetros deste método são explicados a seguir:
modulePath : o caminho do script Node.js a ser executado
args : a lista de parâmetros passada para o script Node.js
options : configurações de parâmetros (não podem ser especificadas), atributos relacionados; como:
detached : veja abaixo para spwan Descrição de options.detached
: Crie o arquivo executável do processo filho execPath
execArgv : A lista de parâmetros de string passada para o arquivo executável, o valor padrão é o valor de process.execArgv
serialization : O tipo de número de série da mensagem entre processos, os valores disponíveis são json e advanced , o valor padrão é json
slient : Se true , stdin , stdout e stderr do processo filho serão passados para o processo pai; através de pipes, caso contrário stdin , stdout e stderr do processo pai serão herdados; o false padrão é
stdio : Veja a descrição de options.stdio em spwan abaixo; O que precisa ser observado aqui é que
slient será ignorado;ipc deve ser incluída (como [0, 1, 2, 'ipc'] ); exceção será lançada.As propriedades cwd , env , uid , gid , windowsVerbatimArguments , signal , timeout e killSignal foram introduzidas acima e não serão repetidas aqui.
const { spawn } = require('child_process');
const ls = spawn('ls', ['-al']);
ls.stdout.on('dados', (dados) => {
console.log(`stdout: ${dados}`);
});
ls.stderr.on('dados', (dados) => {
console.error(`stderr: ${dados}`);
});
ls.on('fechar', (código) => {
console.log(`processo filho encerrado com código ${code}`);
}); fork método é o método básico do módulo child_process , exec e execFile eventualmente chamará spawn para criar um processo filho.
Os parâmetros deste método são explicados a seguir:
command : o nome ou caminho do arquivo executável;
args : a lista de parâmetros passada para o arquivo executável
options : configurações de parâmetros (não podem ser especificadas), os atributos relevantes são os seguintes:
argv0 : enviado ao processo filho valor argv[0], o valor padrão é o valor do command
detached : se deve permitir que o processo filho seja executado independentemente do processo pai (ou seja, após a saída do processo pai, o filho). processo pode continuar a ser executado), o valor padrão é false e quando seu valor é true , cada plataforma O efeito é o seguinte:
Windows , após a saída do processo pai, o processo filho pode continuar a ser executado e o processo filho tem sua própria janela de console (uma vez iniciado este recurso, ele não pode ser alterado durante o processo em execuçãoWindows , o processo filho servirá como líder do novo grupo de sessão de processo neste momento); Se o processo filho está separado do processo pai, o processo filho pode continuar a ser executado após a saída do processo pai.Deve-se notar que se o processo filho precisar realizar uma tarefa de longo prazo e quiser que o processo pai saia mais cedo, os seguintes pontos precisam ser atendidos ao mesmo tempo:
unref do processo filho para remover o filho processo do loop de eventos do processo paidetached definido truestdio ignorePor exemplo, o exemplo a seguir:
// hello.js
const fs = requer('fs');
deixe índice = 0;
função executar() {
setTimeout(() => {
fs.writeFileSync('./hello', `index: ${index}`);
se (índice <10) {
índice += 1;
correr();
}
}, 1000);
}
correr();
//principal.js
const { spawn } = require('child_process');
const filho = spawn('nó', ['./hello.js'], {
desapegado: verdadeiro,
stdio: 'ignorar'
});
child.unref();stdio : configuração padrão de entrada e saída do processo filho, o valor padrão é pipe , o valor é uma string ou array:
pipe é convertido em ['pipe', 'pipe', 'pipe'] ), os valores disponíveis são pipe , overlapped , ignore , inherit ;stdin , stdout e stderr respectivamente, cada um dos valores disponíveis do item são pipe , overlapped , ignore , inherit , ipc , objeto Stream, número inteiro positivo (o descritor de arquivo aberto no processo pai), null (se for localizado nos três primeiros itens do array, é equivalente a pipe , caso contrário é equivalente a ignore ), undefined (se estiver localizado nos três primeiros itens do array, é equivalente a pipe , caso contrário é equivalente a ignore ).Os atributos cwd , env , uid , gid , serialization , shell (o valor é boolean ou string ), windowsVerbatimArguments , windowsHide , signal , timeout , killSignal foram introduzidos acima e não serão repetidos aqui.
O texto acima fornece uma breve introdução ao uso dos métodos principais no módulo child_process Como execSync , execFileSync , forkSync e spwanSync são versões síncronas de exec , execFile e spwan , não há diferença em seus parâmetros. eles não serão repetidos.
Através do módulo cluster , podemos criar um cluster de processo Node.js. Ao adicionar o processo Node.js ao cluster, podemos aproveitar ao máximo as vantagens do multi-core e distribuir tarefas do programa para diferentes processos para melhorar a execução. eficiência do programa; abaixo, usaremos Este exemplo apresenta o uso do módulo cluster :
const http = require('http');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isPrimary) {
for (seja i = 0; i < numCPUs; i++) {
cluster.fork();
}
} outro {
http.createServer((req, res) => {
res.writeHead(200);
res.end(`${process.pid}n`);
}).ouvir(8000);
} O exemplo acima é dividido em duas partes com base no julgamento do atributo cluster.isPrimary (ou seja, julgar se o processo atual é o processo principal):
cluster.fork8000 ).Execute o exemplo acima e acesse http://localhost:8000/ no navegador. Descobriremos que pid retornado é diferente para cada acesso, o que mostra que a solicitação é de fato distribuída para cada processo filho. A estratégia de balanceamento de carga padrão adotada pelo Node.js é o agendamento round-robin, que pode ser modificado através da variável de ambiente NODE_CLUSTER_SCHED_POLICY ou cluster.schedulingPolicy :
NODE_CLUSTER_SCHED_POLICY = rr // ou none cluster.schedulingPolicy = cluster.SCHED_RR; // ou cluster.SCHED_NONE
Outra coisa a observar é que, embora cada processo filho tenha criado um servidor HTTP e escutado a mesma porta, isso não significa que esses processos filhos estejam livres para competir. solicitações do usuário, porque isso não pode garantir que a carga de todos os processos filhos seja equilibrada. Portanto, o processo correto deve ser o processo principal escutar a porta e, em seguida, encaminhar a solicitação do usuário para um subprocesso específico para processamento de acordo com a política de distribuição.
Como os processos são isolados uns dos outros, os processos geralmente se comunicam por meio de mecanismos como memória compartilhada, passagem de mensagens e pipes. Node.js completa a comunicação entre processos pai e filho por meio de消息传递, como no exemplo a seguir:
const http = require('http');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isPrimary) {
for (seja i = 0; i < numCPUs; i++) {
const trabalhador = cluster.fork();
trabalhador.on('mensagem', (mensagem) => {
console.log(`Sou primário(${process.pid}), recebi mensagem do trabalhador: "${message}"`);
trabalhador.send(`Enviar mensagem para trabalhador`)
});
}
} outro {
process.on('mensagem', (mensagem) => {
console.log(`Sou trabalhador(${process.pid}), recebi mensagem do primário: "${message}"`)
});
http.createServer((req, res) => {
res.writeHead(200);
res.end(`${process.pid}n`);
process.send('Enviar mensagem para primário');
}).ouvir(8000);
} Execute o exemplo acima e visite http://localhost:8000/ , em seguida, verifique o terminal, veremos uma saída semelhante a esta:
Sou primário (44460), recebi mensagem do trabalhador: "Enviar mensagem para primário" Sou trabalhador (44461), recebi mensagem do primário: "Enviar mensagem para trabalhador" Sou primário (44460), recebi mensagem do trabalhador: "Enviar mensagem para primário" Sou trabalhador (44462), recebi mensagem do primário: "Enviar mensagem ao trabalhador"
Utilizando este mecanismo, podemos monitorar o status de cada processo filho para que quando ocorrer um acidente em um processo filho, possamos intervir a tempo para garantir a disponibilidade dos serviços.
A interface do módulo cluster é muito simples. Para economizar espaço, aqui fazemos apenas algumas declarações especiais sobre o método cluster.setupPrimary . Para outros métodos, verifique a documentação oficial:
cluster.setupPrimary ser chamado, as configurações relevantes. será sincronizado com o atributo cluster.settings e cada chamada é baseada no valor do atributo cluster.settings atualcluster.setupPrimary ser chamado, não tem impacto no processo filho em execução, apenas nas chamadas cluster.fork subsequentes; são afetados;cluster.setupPrimary é chamado, isso não afeta as passagens subsequentes para cluster.fork O parâmetro env da chamadacluster.setupPrimary só pode ser usado no processo principal.Introduzimos cluster anteriormente, por meio do qual podemos criar um cluster de processos Node.js para melhorar a eficiência de execução do programa. No entanto, cluster é baseado no modelo multiprocesso, com alternância de alto custo entre processos e isolamento. de recursos entre processos O aumento no número de processos filhos pode facilmente levar ao problema de incapacidade de resposta devido a restrições de recursos do sistema. Para resolver tais problemas, o Node.js fornece worker_threads . Abaixo apresentamos brevemente o uso deste módulo por meio de exemplos específicos:
// server.js.
const http = requer('http');
const {Trabalhador} = require('worker_threads');
http.createServer((req, res) => {
const httpWorker = new Worker('./http_worker.js');
httpWorker.on('mensagem', (resultado) => {
res.writeHead(200);
res.end(`${resultado}n`);
});
httpWorker.postMessage('Tom');
}).ouvir(8000);
// http_worker.js
const {parentPort} = require('worker_threads');
parentPort.on('mensagem', (nome) => {
parentPort.postMessage(`Bem-vindo ${nome}!`);
}); O exemplo acima mostra o uso simples de worker_threads , você precisa prestar atenção aos seguintes pontos worker_threads
Crie uma instância Worker por meio de worker_threads.Worker , onde o script Worker pode ser um arquivo JavaScript independente ou字符串, por exemplo, o exemplo acima pode ser modificado como:
const code = "const { parentPort } = require('worker_threads'); parentPort.on('message', (name) => {parentPort.postMessage(`Welcone ${ nome}!` );})";
const httpWorker = new Worker(code, { eval: true });Ao criar uma instância Worker por meio de worker_threads.Worker , você pode definir os metadados iniciais do subthread Worker especificando o valor de workerData , como:
// server .js
const {Trabalhador} = require('worker_threads');
const httpWorker = new Worker('./http_worker.js', { trabalhadorData: { nome: 'Tom'} });
// http_worker.js
const {trabalhadorData} = require('worker_threads');
console.log(workerData);Ao criar uma instância Worker por meio de worker_threads.Worker , você pode definir SHARE_ENV para perceber a necessidade de compartilhar variáveis de ambiente entre o subthread Worker e o thread principal, por exemplo:
const { Worker, SHARE_ENV } = require('worker_threads');
const trabalhador = novo Trabalhador ('process.env.SET_IN_WORKER = "foo"', { eval: true, env: SHARE_ENV });
trabalhador.on('sair', () => {
console.log(process.env.SET_IN_WORKER);
});Diferente do mecanismo de comunicação entre processos em cluster , worker_threads usa MessageChannel para se comunicar entre threads:
parentPort.postMessage e processa mensagens do thread principal ouvindo message principal. evento message da mensagem parentPort ;httpWorker por meio do método postMessage da instância do subthread Worker (aqui está httpWorker e é substituído por este subthread Worker abaixo) e processa mensagens do subthread Worker ouvindo o evento message de httpWorker .No Node.js, seja um processo filho criado por cluster ou um thread filho Worker criado por worker_threads , todos eles têm sua própria instância V8 e loop de eventos. A diferença é que
Embora pareça que os subthreads de trabalho sejam mais eficientes do que os processos filhos, os subthreads de trabalho também apresentam deficiências, ou seja, cluster fornece balanceamento de carga, enquanto worker_threads exigem que concluamos o design e a implementação do balanceamento de carga por nós mesmos.
Este artigo apresenta o uso dos três módulos child_process , cluster e worker_threads em Node.js. Por meio desses três módulos, podemos aproveitar ao máximo as vantagens das CPUs multi-core e resolver com eficiência alguns problemas especiais em um multi-thread (. modo thread) A eficiência operacional de tarefas (como IA, processamento de imagem, etc.). Cada módulo tem seus cenários aplicáveis. Este artigo explica apenas seu uso básico. Como usá-lo de forma eficiente com base em seus próprios problemas, ainda precisa ser explorado por você mesmo. Finalmente, se houver algum erro neste artigo, espero que você possa corrigi-lo. Desejo a todos uma boa codificação todos os dias.