Node.js-underordnede processer: Alt hvad du behøver at vide
Sådan bruges spawn (), exec (), execFile () og fork ()
Opdatering: Denne artikel er nu en del af min bog "Node.js Beyond The Basics".Læs den opdaterede version af dette indhold og mere om Node på jscomplete.com/node-beyond-basics .
Single-threaded, ikke-blokerende ydeevne i Node.js fungerer godt til en enkelt proces. Men til sidst vil en proces i en CPU ikke være nok til at håndtere den stigende arbejdsbyrde for din applikation.
Uanset hvor kraftig din server måtte være, kan en enkelt tråd kun understøtte en begrænset belastning.
Det faktum, at Node.js kører i en enkelt tråd, betyder ikke, at vi ikke kan udnytte flere processer og selvfølgelig flere maskiner også.
Brug af flere processer er den bedste måde at skalere en Node-applikation på. Node.js er designet til at opbygge distribuerede applikationer med mange noder. Dette er grunden til, at det hedder Node . Skalerbarhed er bagt ind på platformen, og det er ikke noget, du begynder at tænke på senere i en applikations levetid.
Denne artikel er en opskrivning af en del af mit Pluralsight-kursus om Node.js. Jeg dækker lignende indhold i videoformat der.Bemærk, at du har brug for en god forståelse af Node.js- begivenheder og streams, før du læser denne artikel. Hvis du ikke allerede har gjort det, anbefaler jeg, at du læser disse to andre artikler, før du læser denne:
Forståelse af Node.js hændelsesdrevet arkitektur
De fleste af Nodes objekter - som HTTP-anmodninger, svar og streams - implementerer EventEmitter-modulet, så de kan ...
Streams: Alt hvad du behøver at vide
Node.js-streams har ry for at være svære at arbejde med og endnu sværere at forstå. Nå, jeg har gode nyheder ...
Modulet Child Processes
Vi kan nemt dreje en underordnet proces ved hjælp af Nodes child_process
modul, og disse underordnede processer kan let kommunikere med hinanden med et messaging-system.
Den child_process
Modulet gør det muligt for os at få adgang til operativsystem funktionaliteter ved at køre ethvert system kommando inde i en, ja, barn proces.
Vi kan kontrollere den underordnede procesindgangsstrøm og lytte til dens outputstrøm. Vi kan også styre de argumenter, der skal sendes til den underliggende OS-kommando, og vi kan gøre hvad vi vil med kommandoens output. Vi kan for eksempel rør output fra en kommando som input til en anden (ligesom vi gør i Linux), da alle input og output fra disse kommandoer kan præsenteres for os ved hjælp af Node.js-streams.
Bemærk, at eksempler, jeg bruger i denne artikel, alle er Linux-baserede. På Windows skal du skifte de kommandoer, jeg bruger med deres Windows-alternativer.
Der er fire forskellige måder at skabe et barn proces i Node: spawn()
, fork()
, exec()
, og execFile()
.
Vi vil se forskellene mellem disse fire funktioner, og hvornår de skal bruges.
Gavede børneprocesser
Den spawn
funktion lancerer en kommando i en ny proces, og vi kan bruge det til at passere denne kommando argumenter. For eksempel er her kode til at gyde en ny proces, der udfører pwd
kommandoen.
const { spawn } = require('child_process'); const child = spawn('pwd');
Vi destruerer simpelthen spawn
funktionen ud af child_process
modulet og udfører den med OS-kommandoen som det første argument.
Resultatet af udførelsen af spawn
funktionen ( child
objektet ovenfor) er en ChildProcess
forekomst, der implementerer EventEmitter API. Dette betyder, at vi kan registrere håndterere til begivenheder på dette underordnede objekt direkte. For eksempel kan vi gøre noget, når barneprocessen afslutter ved at registrere en handler til exit
begivenheden:
child.on('exit', function (code, signal) { console.log('child process exited with ' + `code ${code} and signal ${signal}`); });
Handleren ovenfor giver os udgangen code
til barneprocessen og den signal
, hvis nogen, der blev brugt til at afslutte barneprocessen. Denne signal
variabel er nul, når den underordnede proces afsluttes normalt.
De andre begivenheder, som vi kan registrere handlere til de ChildProcess
tilfælde er disconnect
, error
, close
, og message
.
- Den
disconnect
begivenhed udsendes, når den overordnede proces kalder manueltchild.disconnect
funktion. - Den
error
begivenhed udsendes, hvis processen ikke kunne udklækkede eller dræbt. - Den
close
begivenhed der udsendes, nårstdio
strømme af et barns proces bliver lukket. - Den
message
begivenhed er den vigtigste. Det udsendes, når barneprocessen brugerprocess.send()
funktionen til at sende meddelelser. Sådan kan forældre / barn-processer kommunikere med hinanden. Vi ser et eksempel på dette nedenfor.
Ethvert barn proces også får de tre standard stdio
vandløb, som vi kan få adgang til at bruge child.stdin
, child.stdout
og child.stderr
.
Når disse streams lukkes, udsender den barneproces, der brugte dem, close
begivenheden. Denne close
begivenhed er forskellig fra exit
begivenheden, fordi flere underordnede processer kan dele de samme stdio
streams, og et underordnet proces, der afsluttes, betyder ikke, at streams blev lukket.
Da alle streams er hændelsesemittere, kan vi lytte til forskellige begivenheder i de stdio
streams, der er knyttet til enhver underordnet proces. I modsætning til i en normal proces er stdout
/ stderr
streams dog i en underordnet proces læsbare strømme, mens stdin
strømmen er en skrivbar. Dette er dybest set det omvendte af disse typer, som findes i en hovedproces. De begivenheder, vi kan bruge til disse streams, er de standard. Vigtigst er det, at på de læsbare streams kan vi lytte til data
begivenheden, som har output af kommandoen eller enhver fejl, der opstår under udførelsen af kommandoen:
child.stdout.on('data', (data) => { console.log(`child stdout:\n${data}`); }); child.stderr.on('data', (data) => { console.error(`child stderr:\n${data}`); });
De to håndterere ovenfor logger begge sager til hovedprocessen stdout
og stderr
. Når vi udfører ovennævnte spawn
funktion, bliver output af pwd
kommandoen udskrevet, og underordnet proces afsluttes med kode 0
, hvilket betyder, at der ikke opstod en fejl.
Vi kan sende argumenter til kommandoen, der udføres af spawn
funktionen ved hjælp af det andet argument for spawn
funktionen, som er en matrix med alle de argumenter, der skal sendes til kommandoen. For eksempel for at udføre find
kommandoen i den aktuelle mappe med et -type f
argument (kun for at liste filer) kan vi gøre:
const child = spawn('find', ['.', '-type', 'f']);
Hvis der opstår en fejl under udførelsen af kommandoen, hvis vi f.eks. Finder en ugyldig destination ovenfor, child.stderr
data
udløses exit
hændelseshåndteringen, og hændelseshåndtereren rapporterer en exitkode for 1
, hvilket betyder, at der er opstået en fejl. Fejlværdierne afhænger faktisk af værts-OS og typen af fejl.
En barneproces stdin
er en skrivbar strøm. Vi kan bruge den til at sende en kommando noget input. Ligesom enhver skrivbar strøm er den nemmeste måde at forbruge den på at bruge pipe
funktionen. Vi rør simpelthen en læsbar strøm i en skrivbar strøm. Da hovedprocessen stdin
er en læsbar strøm, kan vi føre den til en underordnet processtrøm stdin
. For eksempel:
const { spawn } = require('child_process'); const child = spawn('wc'); process.stdin.pipe(child.stdin) child.stdout.on('data', (data) => { console.log(`child stdout:\n${data}`); });
In the example above, the child process invokes the wc
command, which counts lines, words, and characters in Linux. We then pipe the main process stdin
(which is a readable stream) into the child process stdin
(which is a writable stream). The result of this combination is that we get a standard input mode where we can type something and when we hit Ctrl+D
, what we typed will be used as the input of the wc
command.

We can also pipe the standard input/output of multiple processes on each other, just like we can do with Linux commands. For example, we can pipe the stdout
of the find
command to the stdin of the wc
command to count all the files in the current directory:
const { spawn } = require('child_process'); const find = spawn('find', ['.', '-type', 'f']); const wc = spawn('wc', ['-l']); find.stdout.pipe(wc.stdin); wc.stdout.on('data', (data) => { console.log(`Number of files ${data}`); });
I added the -l
argument to the wc
command to make it count only the lines. When executed, the code above will output a count of all files in all directories under the current one.
Shell Syntax and the exec function
By default, the spawn
function does not create a shell to execute the command we pass into it. This makes it slightly more efficient than the exec
function, which does create a shell. The exec
function has one other major difference. It buffers the command’s generated output and passes the whole output value to a callback function (instead of using streams, which is what spawn
does).
Here’s the previous find | wc
example implemented with an exec
function.
const { exec } = require('child_process'); exec('find . -type f | wc -l', (err, stdout, stderr) => { if (err) { console.error(`exec error: ${err}`); return; } console.log(`Number of files ${stdout}`); });
Since the exec
function uses a shell to execute the command, we can use the shell syntax directly here making use of the shell pipe feature.
Note that using the shell syntax comes at a security risk if you’re executing any kind of dynamic input provided externally. A user can simply do a command injection attack using shell syntax characters like ; and $ (for example, command + ’; rm -rf ~’
)
The exec
function buffers the output and passes it to the callback function (the second argument to exec
) as the stdout
argument there. This stdout
argument is the command’s output that we want to print out.
The exec
function is a good choice if you need to use the shell syntax and if the size of the data expected from the command is small. (Remember, exec
will buffer the whole data in memory before returning it.)
The spawn
function is a much better choice when the size of the data expected from the command is large, because that data will be streamed with the standard IO objects.
We can make the spawned child process inherit the standard IO objects of its parents if we want to, but also, more importantly, we can make the spawn
function use the shell syntax as well. Here’s the same find | wc
command implemented with the spawn
function:
const child = spawn('find . -type f | wc -l', { stdio: 'inherit', shell: true });
Because of the stdio: 'inherit'
option above, when we execute the code, the child process inherits the main process stdin
, stdout
, and stderr
. This causes the child process data events handlers to be triggered on the main process.stdout
stream, making the script output the result right away.
Because of the shell: true
option above, we were able to use the shell syntax in the passed command, just like we did with exec
. But with this code, we still get the advantage of the streaming of data that the spawn
function gives us. This is really the best of both worlds.
There are a few other good options we can use in the last argument to the child_process
functions besides shell
and stdio
. We can, for example, use the cwd
option to change the working directory of the script. For example, here’s the same count-all-files example done with a spawn
function using a shell and with a working directory set to my Downloads folder. The cwd
option here will make the script count all files I have in ~/Downloads
:
const child = spawn('find . -type f | wc -l', { stdio: 'inherit', shell: true, cwd: '/Users/samer/Downloads' });
Another option we can use is the env
option to specify the environment variables that will be visible to the new child process. The default for this option is process.env
which gives any command access to the current process environment. If we want to override that behavior, we can simply pass an empty object as the env
option or new values there to be considered as the only environment variables:
const child = spawn('echo $ANSWER', { stdio: 'inherit', shell: true, env: { ANSWER: 42 }, });
The echo command above does not have access to the parent process’s environment variables. It can’t, for example, access $HOME
, but it can access $ANSWER
because it was passed as a custom environment variable through the env
option.
One last important child process option to explain here is the detached
option, which makes the child process run independently of its parent process.
Assuming we have a file timer.js
that keeps the event loop busy:
setTimeout(() => { // keep the event loop busy }, 20000);
We can execute it in the background using the detached
option:
const { spawn } = require('child_process'); const child = spawn('node', ['timer.js'], { detached: true, stdio: 'ignore' }); child.unref();
The exact behavior of detached child processes depends on the OS. On Windows, the detached child process will have its own console window while on Linux the detached child process will be made the leader of a new process group and session.
If the unref
function is called on the detached process, the parent process can exit independently of the child. This can be useful if the child is executing a long-running process, but to keep it running in the background the child’s stdio
configurations also have to be independent of the parent.
The example above will run a node script (timer.js
) in the background by detaching and also ignoring its parent stdio
file descriptors so that the parent can terminate while the child keeps running in the background.

The execFile function
If you need to execute a file without using a shell, the execFile
function is what you need. It behaves exactly like the exec
function, but does not use a shell, which makes it a bit more efficient. On Windows, some files cannot be executed on their own, like .bat
or .cmd
files. Those files cannot be executed with execFile
and either exec
or spawn
with shell set to true is required to execute them.
The *Sync function
The functions spawn
, exec
, and execFile
from the child_process
module also have synchronous blocking versions that will wait until the child process exits.
const { spawnSync, execSync, execFileSync, } = require('child_process');
Those synchronous versions are potentially useful when trying to simplify scripting tasks or any startup processing tasks, but they should be avoided otherwise.
The fork() function
The fork
function is a variation of the spawn
function for spawning node processes. The biggest difference between spawn
and fork
is that a communication channel is established to the child process when using fork
, so we can use the send
function on the forked process along with the global process
object itself to exchange messages between the parent and forked processes. We do this through the EventEmitter
module interface. Here’s an example:
The parent file, parent.js
:
const { fork } = require('child_process'); const forked = fork('child.js'); forked.on('message', (msg) => { console.log('Message from child', msg); }); forked.send({ hello: 'world' });
The child file, child.js
:
process.on('message', (msg) => { console.log('Message from parent:', msg); }); let counter = 0; setInterval(() => { process.send({ counter: counter++ }); }, 1000);
In the parent file above, we fork child.js
(which will execute the file with the node
command) and then we listen for the message
event. The message
event will be emitted whenever the child uses process.send
, which we’re doing every second.
To pass down messages from the parent to the child, we can execute the send
function on the forked object itself, and then, in the child script, we can listen to the message
event on the global process
object.
When executing the parent.js
file above, it’ll first send down the { hello: 'world' }
object to be printed by the forked child process and then the forked child process will send an incremented counter value every second to be printed by the parent process.

Let’s do a more practical example about the fork
function.
Let’s say we have an http server that handles two endpoints. One of these endpoints (/compute
below) is computationally expensive and will take a few seconds to complete. We can use a long for loop to simulate that:
const http = require('http'); const longComputation = () => { let sum = 0; for (let i = 0; i { if (req.url === '/compute') { const sum = longComputation(); return res.end(`Sum is ${sum}`); } else { res.end('Ok') } }); server.listen(3000);
This program has a big problem; when the the /compute
endpoint is requested, the server will not be able to handle any other requests because the event loop is busy with the long for loop operation.
There are a few ways with which we can solve this problem depending on the nature of the long operation but one solution that works for all operations is to just move the computational operation into another process using fork
.
We first move the whole longComputation
function into its own file and make it invoke that function when instructed via a message from the main process:
In a new compute.js
file:
const longComputation = () => { let sum = 0; for (let i = 0; i { const sum = longComputation(); process.send(sum); });
Now, instead of doing the long operation in the main process event loop, we can fork
the compute.js
file and use the messages interface to communicate messages between the server and the forked process.
const http = require('http'); const { fork } = require('child_process'); const server = http.createServer(); server.on('request', (req, res) => { if (req.url === '/compute') { const compute = fork('compute.js'); compute.send('start'); compute.on('message', sum => { res.end(`Sum is ${sum}`); }); } else { res.end('Ok') } }); server.listen(3000);
When a request to /compute
happens now with the above code, we simply send a message to the forked process to start executing the long operation. The main process’s event loop will not be blocked.
Once the forked process is done with that long operation, it can send its result back to the parent process using process.send
.
I forældreprocessen lytter vi til message
begivenheden på selve forkedbarnsprocessen. Når vi får den begivenhed, har vi en sum
værdi klar til, at vi kan sende den anmodende bruger via http.
Koden ovenfor er selvfølgelig begrænset af antallet af processer, vi kan forkaste, men når vi udfører det og anmoder om det lange beregningsendepunkt over http, er hovedserveren slet ikke blokeret og kan tage yderligere anmodninger.
Nodes cluster
modul, som er emnet for min næste artikel, er baseret på denne idé om børns procesforfalskning og belastningsafbalancering af anmodningerne blandt de mange gafler, som vi kan oprette på ethvert system.
Det er alt, hvad jeg har til dette emne. Tak for læsningen! Indtil næste gang!
Learning React eller Node? Tjek mine bøger:
- Lær React.js ved at bygge spil
- Node.js ud over det grundlæggende