Comment utiliser spawn (), exec (), execFile () et fork ()
Mise à jour: Cet article fait maintenant partie de mon livre «Node.js Beyond The Basics».Lisez la version mise à jour de ce contenu et plus d'informations sur Node sur jscomplete.com/node-beyond-basics .
Les performances à thread unique et non bloquantes dans Node.js fonctionnent parfaitement pour un seul processus. Mais finalement, un processus dans un processeur ne suffira pas à gérer la charge de travail croissante de votre application.
Quelle que soit la puissance de votre serveur, un seul thread ne peut supporter qu'une charge limitée.
Le fait que Node.js s'exécute dans un seul thread ne signifie pas que nous ne pouvons pas tirer parti de plusieurs processus et, bien sûr, de plusieurs machines.
L'utilisation de plusieurs processus est le meilleur moyen de mettre à l'échelle une application Node. Node.js est conçu pour créer des applications distribuées avec de nombreux nœuds. C'est pourquoi il s'appelle Node . L'évolutivité est intégrée à la plate-forme et vous ne commencez pas à y penser plus tard dans la vie d'une application.
Cet article est une rédaction d'une partie de mon cours Pluralsight sur Node.js. J'y couvre un contenu similaire au format vidéo.Veuillez noter que vous aurez besoin d'une bonne compréhension des événements et des flux Node.js avant de lire cet article. Si vous ne l'avez pas déjà fait, je vous recommande de lire ces deux autres articles avant de lire celui-ci:
Comprendre l'architecture événementielle Node.js
La plupart des objets de Node - comme les requêtes HTTP, les réponses et les flux - implémentent le module EventEmitter afin qu'ils puissent…
Streams: tout ce que vous devez savoir
Les flux Node.js ont la réputation d'être difficiles à utiliser et encore plus difficiles à comprendre. Eh bien, j'ai de bonnes nouvelles ...
Le module des processus enfants
Nous pouvons facilement faire tourner un processus enfant à l'aide du child_process
module de Node et ces processus enfants peuvent facilement communiquer entre eux avec un système de messagerie.
Le child_process
module nous permet d'accéder aux fonctionnalités du système d'exploitation en exécutant n'importe quelle commande système dans un processus enfant.
Nous pouvons contrôler ce flux d'entrée de processus enfant et écouter son flux de sortie. Nous pouvons également contrôler les arguments à passer à la commande du système d'exploitation sous-jacente, et nous pouvons faire ce que nous voulons avec la sortie de cette commande. Nous pouvons, par exemple, diriger la sortie d'une commande comme entrée vers une autre (comme nous le faisons sous Linux) car toutes les entrées et sorties de ces commandes peuvent nous être présentées à l'aide des flux Node.js.
Notez que les exemples que j'utiliserai dans cet article sont tous basés sur Linux. Sous Windows, vous devez changer les commandes que j'utilise avec leurs alternatives Windows.
Il existe quatre façons de créer un processus enfant dans le nœud: spawn()
, fork()
, exec()
et execFile()
.
Nous allons voir les différences entre ces quatre fonctions et quand les utiliser.
Processus enfants engendrés
La spawn
fonction lance une commande dans un nouveau processus et nous pouvons l'utiliser pour passer cette commande tous les arguments. Par exemple, voici le code pour générer un nouveau processus qui exécutera la pwd
commande.
const { spawn } = require('child_process'); const child = spawn('pwd');
Nous déstructurons simplement la spawn
fonction hors du child_process
module et l'exécutons avec la commande OS comme premier argument.
Le résultat de l'exécution de la spawn
fonction (l' child
objet ci-dessus) est une ChildProcess
instance, qui implémente l'API EventEmitter. Cela signifie que nous pouvons enregistrer des gestionnaires d'événements sur cet objet enfant directement. Par exemple, nous pouvons faire quelque chose lorsque le processus enfant se termine en enregistrant un gestionnaire pour l' exit
événement:
child.on('exit', function (code, signal) { console.log('child process exited with ' + `code ${code} and signal ${signal}`); });
Le gestionnaire ci-dessus nous donne la sortie code
du processus enfant et signal
, le cas échéant, qui a été utilisé pour terminer le processus enfant. Cette signal
variable est nulle lorsque le processus enfant se termine normalement.
Les autres événements que nous pouvons enregistrer des gestionnaires pour les ChildProcess
cas sont disconnect
, error
, close
et message
.
- L'
disconnect
événement est émis lorsque le processus parent appelle manuellement lachild.disconnect
fonction. - L'
error
événement est émis si le processus n'a pas pu être généré ou tué. - L'
close
événement est émis lorsque lesstdio
flux d'un processus enfant sont fermés. - L'
message
événement est le plus important. Il est émis lorsque le processus enfant utilise laprocess.send()
fonction pour envoyer des messages. C'est ainsi que les processus parents / enfants peuvent communiquer entre eux. Nous en verrons un exemple ci-dessous.
Chaque processus enfant reçoit aussi les trois standards stdio
courants, que l' on peut accéder à l' aide child.stdin
, child.stdout
et child.stderr
.
Lorsque ces flux sont fermés, le processus enfant qui les utilisait émettra l' close
événement. Cet close
événement est différent de l' exit
événement car plusieurs processus enfants peuvent partager les mêmes stdio
flux et la fermeture d'un processus enfant ne signifie pas que les flux ont été fermés.
Étant donné que tous les flux sont des émetteurs d'événements, nous pouvons écouter différents événements sur les stdio
flux associés à chaque processus enfant. Contrairement à un processus normal, dans un processus enfant, les flux stdout
/ stderr
sont des flux lisibles alors que le stdin
flux est un flux inscriptible. C'est fondamentalement l'inverse de ces types que l'on trouve dans un processus principal. Les événements que nous pouvons utiliser pour ces flux sont les événements standard. Plus important encore, sur les flux lisibles, nous pouvons écouter l' data
événement, qui aura la sortie de la commande ou toute erreur rencontrée lors de l'exécution de la commande:
child.stdout.on('data', (data) => { console.log(`child stdout:\n${data}`); }); child.stderr.on('data', (data) => { console.error(`child stderr:\n${data}`); });
Les deux gestionnaires ci-dessus enregistreront les deux cas dans le processus principal stdout
et stderr
. Lorsque nous exécutons la spawn
fonction ci-dessus, la sortie de la pwd
commande est imprimée et le processus enfant se termine avec du code 0
, ce qui signifie qu'aucune erreur ne s'est produite.
Nous pouvons passer des arguments à la commande exécutée par la spawn
fonction en utilisant le deuxième argument de la spawn
fonction, qui est un tableau de tous les arguments à passer à la commande. Par exemple, pour exécuter la find
commande sur le répertoire courant avec un -type f
argument (pour lister les fichiers uniquement), on peut faire:
const child = spawn('find', ['.', '-type', 'f']);
Si une erreur se produit pendant l'exécution de la commande, par exemple, si nous donnons find une destination invalide ci-dessus, le child.stderr
data
gestionnaire d'événements sera déclenché et le exit
gestionnaire d'événements rapportera un code de sortie de 1
, ce qui signifie qu'une erreur s'est produite. Les valeurs d'erreur dépendent en fait du système d'exploitation hôte et du type d'erreur.
Un processus enfant stdin
est un flux accessible en écriture. Nous pouvons l'utiliser pour envoyer une commande une entrée. Comme tout flux accessible en écriture, le moyen le plus simple de le consommer est d'utiliser la pipe
fonction. Nous conduisons simplement un flux lisible dans un flux accessible en écriture. Étant donné que le processus principal stdin
est un flux lisible, nous pouvons le diriger vers un stdin
flux de processus enfant . Par exemple:
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}`); });
Dans l'exemple ci-dessus, le processus enfant appelle la wc
commande, qui compte les lignes, les mots et les caractères sous Linux. Nous dirigons ensuite le processus principal stdin
(qui est un flux lisible) dans le processus enfant stdin
(qui est un flux accessible en écriture). Le résultat de cette combinaison est que nous obtenons un mode d'entrée standard où nous pouvons taper quelque chose et quand nous frappons Ctrl+D
, ce que nous avons tapé sera utilisé comme entrée de la wc
commande.

Nous pouvons également diriger les entrées / sorties standard de plusieurs processus les uns sur les autres, comme nous pouvons le faire avec les commandes Linux. Par exemple, nous pouvons diriger le stdout
de la find
commande vers le stdin de la wc
commande pour compter tous les fichiers dans le répertoire courant:
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}`); });
J'ai ajouté l' -l
argument à la wc
commande pour qu'elle ne compte que les lignes. Une fois exécuté, le code ci-dessus affichera un décompte de tous les fichiers dans tous les répertoires sous le répertoire actuel.
Syntaxe du shell et fonction exec
Par défaut, la spawn
fonction ne crée pas de shell pour exécuter la commande que nous lui transmettons. Cela le rend légèrement plus efficace que la exec
fonction, qui crée un shell. La exec
fonction a une autre différence majeure. Il met en mémoire tampon la sortie générée par la commande et transmet toute la valeur de sortie à une fonction de rappel (au lieu d'utiliser des flux, ce qui est le spawn
cas).
Voici l' find | wc
exemple précédent implémenté avec une exec
fonction.
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
.
In the parent process, we listen to the message
event on the forked child process itself. When we get that event, we’ll have a sum
value ready for us to send to the requesting user over http.
The code above is, of course, limited by the number of processes we can fork, but when we execute it and request the long computation endpoint over http, the main server is not blocked at all and can take further requests.
Node’s cluster
module, which is the topic of my next article, is based on this idea of child process forking and load balancing the requests among the many forks that we can create on any system.
That’s all I have for this topic. Thanks for reading! Until next time!
Learning React or Node? Checkout my books:
- Learn React.js by Building Games
- Node.js Beyond the Basics