Node.jsの子プロセス:知っておくべきことすべて

spawn()、exec()、execFile()、およびfork()の使用方法

更新:この記事は私の本「Node.jsBeyondTheBasics」の一部になりました。

jscomplete.com/node-beyond-basicsで、このコンテンツの更新バージョンとNodeの詳細をお読みください

Node.jsのシングルスレッド、ノンブロッキングパフォーマンスは、単一のプロセスに最適です。しかし、最終的には、1つのCPUの1つのプロセスでは、アプリケーションの増加するワークロードを処理するのに十分ではなくなります。

サーバーがどれほど強力であっても、単一のスレッドは限られた負荷しかサポートできません。

Node.jsが単一のスレッドで実行されるという事実は、複数のプロセス、そしてもちろん複数のマシンも利用できないことを意味するものではありません。

複数のプロセスを使用することは、ノードアプリケーションをスケーリングするための最良の方法です。Node.jsは、多くのノードを持つ分散アプリケーションを構築するために設計されています。これがノードという名前の理由です。スケーラビリティはプラットフォームに組み込まれており、アプリケーションの存続期間の後半で考え始めるものではありません。

この記事は、Node.jsに関する私のPluralsightコースの一部をまとめたものです。私はそこで同様のコンテンツをビデオ形式でカバーしています。

この記事を読む前に、Node.jsのイベントストリームを十分に理解する必要があることに注意してください。まだ読んでいない場合は、この記事を読む前に、他の2つの記事を読むことをお勧めします。

Node.jsイベント駆動型アーキテクチャを理解する

HTTPリクエスト、レスポンス、ストリームなど、ノードのほとんどのオブジェクトはEventEmitterモジュールを実装しているため、…

ストリーム:あなたが知る必要があるすべて

Node.jsストリームは、操作が難しく、さらに理解しにくいという評判があります。良いニュースがあります…

子プロセスモジュール

Nodeのchild_processモジュールを使用して子プロセスを簡単にスピンでき、それらの子プロセスはメッセージングシステムを使用して相互に簡単に通信できます。

このchild_processモジュールを使用すると、子プロセス内で任意のシステムコマンドを実行することにより、オペレーティングシステムの機能にアクセスできます。

その子プロセスの入力ストリームを制御し、その出力ストリームをリッスンできます。基になるOSコマンドに渡される引数を制御することもでき、そのコマンドの出力を使用して必要なことを実行できます。たとえば、Node.jsストリームを使用してこれらのコマンドのすべての入力と出力を提示できるため、あるコマンドの出力を別のコマンドへの入力としてパイプ処理できます(Linuxの場合と同じように)。

この記事で使用する例はすべてLinuxベースであることに注意してください。Windowsでは、私が使用するコマンドをWindowsの代替手段と切り替える必要があります。

ノード内の子プロセスを作成するには、4つの異なる方法がありますspawn()fork()exec()、とexecFile()

これらの4つの機能の違いと、それぞれをいつ使用するかを見ていきます。

スポーンされた子プロセス

このspawn関数は新しいプロセスでコマンドを起動し、それを使用してそのコマンドに任意の引数を渡すことができます。たとえば、pwdコマンドを実行する新しいプロセスを生成するコードを次に示します。

const { spawn } = require('child_process'); const child = spawn('pwd');

私たちは、単にdestructurespawn外の機能をchild_processモジュールと、最初の引数としてOSのコマンドでそれを実行します。

spawn関数(child上記のオブジェクト)を実行した結果はChildProcess、EventEmitterAPIを実装するインスタンスです。これは、この子オブジェクトのイベントのハンドラーを直接登録できることを意味します。たとえば、exitイベントのハンドラーを登録することで、子プロセスが終了したときに何かを行うことができます。

child.on('exit', function (code, signal) { console.log('child process exited with ' + `code ${code} and signal ${signal}`); });

上記のハンドラーcodeは、子プロセスの出口と、子プロセスsignalを終了するために使用された出口(存在する場合)を提供します。signal子プロセスが正常に終了する場合、この変数はnullです。

私たちはとのハンドラを登録できることを他のイベントChildProcessのインスタンスがありdisconnecterrorclose、とmessage

  • disconnect親プロセスが手動で呼び出したときにイベントが発せられるchild.disconnect機能。
  • errorプロセスが起動されたり殺されなかった場合にイベントが放出されます。
  • closeときにイベントが放出されたstdio子プロセスのストリームは閉じられます。
  • messageイベントは、最も重要なものです。子プロセスがprocess.send()関数を使用してメッセージを送信するときに発行されます。これは、親/子プロセスが相互に通信する方法です。この例を以下に示します。

すべての子プロセスはまた、3つの標準を取得stdio私たちが使用してアクセスすることができますストリームをchild.stdinchild.stdoutchild.stderr

これらのストリームが閉じられると、それらを使用していた子プロセスがcloseイベントを発行します。このcloseイベントはexit、複数の子プロセスが同じstdioストリームを共有する可能性があるため、イベントとは異なります。したがって、1つの子プロセスが終了しても、ストリームが閉じられたわけではありません。

Since all streams are event emitters, we can listen to different events on those stdio streams that are attached to every child process. Unlike in a normal process though, in a child process, the stdout/stderr streams are readable streams while the stdin stream is a writable one. This is basically the inverse of those types as found in a main process. The events we can use for those streams are the standard ones. Most importantly, on the readable streams, we can listen to the data event, which will have the output of the command or any error encountered while executing the command:

child.stdout.on('data', (data) => { console.log(`child stdout:\n${data}`); }); child.stderr.on('data', (data) => { console.error(`child stderr:\n${data}`); });

The two handlers above will log both cases to the main process stdout and stderr. When we execute the spawn function above, the output of the pwd command gets printed and the child process exits with code 0, which means no error occurred.

We can pass arguments to the command that’s executed by the spawn function using the second argument of the spawn function, which is an array of all the arguments to be passed to the command. For example, to execute the find command on the current directory with a -type f argument (to list files only), we can do:

const child = spawn('find', ['.', '-type', 'f']);

If an error occurs during the execution of the command, for example, if we give find an invalid destination above, the child.stderrdata event handler will be triggered and the exit event handler will report an exit code of 1, which signifies that an error has occurred. The error values actually depend on the host OS and the type of error.

A child process stdin is a writable stream. We can use it to send a command some input. Just like any writable stream, the easiest way to consume it is using the pipe function. We simply pipe a readable stream into a writable stream. Since the main process stdin is a readable stream, we can pipe that into a child process stdin stream. For example:

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.

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