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
cwd
windowsVerbatimArguments
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 true
stdio
ignore
Por 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.fork
8000
).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.