Node.js子进程:你想要知道的一切
<div><img src="https://img2023.cnblogs.com/blog/51946/202310/51946-20231031161151044-1714794557.png" alt="" style="display: block; margin-left: auto; margin-right: auto"></div><h2>如何使用spawn(),exec(),execFile()和fork()</h2>
<p> 对于单进程而言,Node.js的单线程和非阻塞特性表现地非常好。然而,对于处理功能越来越复杂的应用程序而言,一个单进程的CPU是远远无法满足需要的。</p>
<p> 无论你的服务器有多强大,单线程都是远远不够用的。</p>
<p> 事实上,Node.js的单线程特性并不意味着我们不能将其运行在多线程或者多服务器的环境中。</p>
<p> 使用多进程是扩展Node.js应用程序的最佳实践,Node.js就是为构建具有多个节点的分布式应用程序而设计的,这也是它为什么被命名为<em>Node</em>的原因。可伸缩性已经融入到平台中,而不应该在应用程序开发的后期才开始考虑这部分内容。</p>
<p> 在阅读本文之前,你可能需要对Node.js的事件和流有一个很好的理解,推荐阅读下面这两篇文章:</p>
<p> <strong>理解Node.js事件驱动架构</strong></p>
<p> <strong>Stream:你想要知道的一切</strong></p>
<h2>child_process模块</h2>
<p> 通过使用Node.js的<span style="background-color: rgba(192, 192, 192, 1)">child_process</span>模块,我们可以非常轻松地调用子进程,并在各个子进程之间通过消息系统相互通信。</p>
<p> <span style="background-color: rgba(192, 192, 192, 1)">child_process</span>模块使我们可以通过运行在其中的系统命令来访问操作系统的功能。</p>
<p> 我们可以控制子进程的输入流,并监听它的输出流。我们还可以控制传递给底层操作系统的命令的参数,并对这些命令的输出做任何我们想做的事情。例如,我们可以将一个命令的输出作为另一个命令的输入(就像我们在Linux系统中做的那样),因为所有这些命令的输入和输出都可以用Node.js流的形式呈现。</p>
<p> <em>注意,本文中所使用的示例都是基于Linux的。在Windows上,你需要将这些命令转换成Windows上对应的部分。</em></p>
<p> 在Node.js中,有四种不同的方式可以用来创建子进程:<span style="background-color: rgba(192, 192, 192, 1)">spawn()</span>,<span style="background-color: rgba(192, 192, 192, 1)">fork()</span>,<span style="background-color: rgba(192, 192, 192, 1)">exec()</span>和<span style="background-color: rgba(192, 192, 192, 1)">execFile()</span>。</p>
<p> 接下来让我们看看这四种方法之间的区别以及何时使用它们。</p>
<h3>产生一个子进程</h3>
<p> <span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数会创建一个子进程,并在其中执行一个命令,我们可以通过它向该命令传递任何参数。例如,下面的代码创建了一个新的进程并在其中执行<span style="background-color: rgba(192, 192, 192, 1)">pwd</span>命令。</p>
<div class="cnblogs_code">
<pre>const { spawn } = require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
const child </span>= spawn('pwd');</pre>
</div>
<p> 我们只需要引入<span style="background-color: rgba(192, 192, 192, 1)">child_process</span>模块,并使用其中的<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数,将命令作为第一个参数传入即可执行该命令。</p>
<p> <span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数(上面代码中的child对象)会返回<span style="background-color: rgba(192, 192, 192, 1)">ChildProcess</span>的一个实例,该实例实现了EventEmitter API,这意味着我们可以直接在它上面注册事件处理程序。例如,我们可以为其注册一个事件,当子进程退出时执行某些操作。</p>
<div class="cnblogs_code">
<pre>child.on('exit', <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (code, signal) {
console.log(</span>'child process exited with ' +<span style="color: rgba(0, 0, 0, 1)">
`code ${code} and signal ${signal}`);
});</span></pre>
</div>
<p> 上面的代码提供了子进程退出或者终止时的<span style="background-color: rgba(192, 192, 192, 1)">code</span>和<span style="background-color: rgba(192, 192, 192, 1)">signal</span>。只有当子进程正常退出时,参数<span style="background-color: rgba(192, 192, 192, 1)">signal</span>的值才是null。</p>
<p> 我们还可以为<span style="background-color: rgba(192, 192, 192, 1)">ChildProcess</span>实例注册其它事件:<span style="background-color: rgba(192, 192, 192, 1)">disconnect</span>、<span style="background-color: rgba(192, 192, 192, 1)">error</span>、<span style="background-color: rgba(192, 192, 192, 1)">close</span>和<span style="background-color: rgba(192, 192, 192, 1)">message</span>。</p>
<ul>
<li>当在父进程中手动调用<span style="background-color: rgba(192, 192, 192, 1)">child.disconnect</span>函数时,<span style="background-color: rgba(192, 192, 192, 1)">disconnect</span>事件被触发。</li>
<li>如果无法创建或终止子进程,<span style="background-color: rgba(192, 192, 192, 1)">error</span>事件被触发。</li>
<li>当子进程的<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>流被关闭时,<span style="background-color: rgba(192, 192, 192, 1)">close</span>事件被触发。</li>
<li><span style="background-color: rgba(192, 192, 192, 1)">message</span>事件是最重要的一个事件。当子进程使用<span style="background-color: rgba(192, 192, 192, 1)">process.send()</span>函数发送消息时,该事件被触发。父进程和子进程之间通过这种方式相互通信。下面我们会通过一个示例来讲解这部分内容。</li>
</ul>
<p> 每一个子进程都可以通过<span style="background-color: rgba(192, 192, 192, 1)">child.stdin</span>,<span style="background-color: rgba(192, 192, 192, 1)">child.stdout</span>和<span style="background-color: rgba(192, 192, 192, 1)">child.stderr</span>来获取三个标准的<span style="background-color: rgba(192, 192, 192, 1)">stdoi</span>流。</p>
<p> 当这些流被关闭时,使用它们的子进程会触发<span style="background-color: rgba(192, 192, 192, 1)">close</span>事件。<span style="background-color: rgba(192, 192, 192, 1)">close</span>事件与<span style="background-color: rgba(192, 192, 192, 1)">exit</span>事件有所不同,因为多个不同的子进程之间可以共享同一个<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>流,所以一个子进程的退出并不意味着其它的流已经关闭。</p>
<p> 由于所有的流都是事件发射器,因此我们可以对附加在每个子进程上的<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>流监听不同的事件。但是这与普通的进程不同,在子进程中,<span style="background-color: rgba(192, 192, 192, 1)">stdout</span>/<span style="background-color: rgba(192, 192, 192, 1)">stderr</span>流是可读流,而<span style="background-color: rgba(192, 192, 192, 1)">stdin</span>流是可写流。这与我们在主进程中所遇到的情况正好相反。我们可以为这些流使用的事件都是标准事件。最重要的是,我们可以对可读流监听<span style="background-color: rgba(192, 192, 192, 1)">data</span>事件,其中包含命令的输出或者执行命令时遇到的错误:</p>
<div class="cnblogs_code">
<pre>child.stdout.on('data', (data) =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(`child stdout:\n${data}`);
});
child.stderr.on(</span>'data', (data) =><span style="color: rgba(0, 0, 0, 1)"> {
console.error(`child stderr:\n${data}`);
});</span></pre>
</div>
<p> 上面的两个处理程序将这两种情况的结果记录到主进程的<span style="background-color: rgba(192, 192, 192, 1)">stdout</span>和<span style="background-color: rgba(192, 192, 192, 1)">stderr</span>中。当我们执行上面的<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数时,<span style="background-color: rgba(192, 192, 192, 1)">pwd</span>命令的执行结构会被打印出来,然后子进程退出,code的值是<span style="background-color: rgba(192, 192, 192, 1)">0</span>,意思是没有发生任何错误。</p>
<p> 我们可以将执行命令时的参数作为调用<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数的第二个参数,该参数是一个数组,因此可以将执行命令时的所有参数作为数组的元素传递给<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数。例如,要在当前目录中执行<span style="background-color: rgba(192, 192, 192, 1)">find</span>命令(仅列出文件),并使用<span style="background-color: rgba(192, 192, 192, 1)">-type f</span>参数,我们可以这样:</p>
<div class="cnblogs_code">
<pre>const child = spawn('find', ['.', '-type', 'f']);</pre>
</div>
<p> 如果在执行命令时发生错误,例如上面的例子中如果当前路径无效,则<span style="background-color: rgba(192, 192, 192, 1)">child.stderr</span> <span style="background-color: rgba(192, 192, 192, 1)">data</span>事件将会被触发,同时也会触发<span style="background-color: rgba(192, 192, 192, 1)">exit</span>事件,此时code的值为<span style="background-color: rgba(192, 192, 192, 1)">1</span>,表示有错误发生。错误的值实际上取决于主机操作系统和错误的类型。</p>
<p> 子进程<span style="background-color: rgba(192, 192, 192, 1)">stdin</span>是可写流。我们可以使用它向命令发送一些数据。与任何其它的可写流一样,我们可以简单地通过<span style="background-color: rgba(192, 192, 192, 1)">pipe</span>函数来使用它。我们只需要简单地将可读流pipe到可写流。由于主进程<span style="background-color: rgba(192, 192, 192, 1)">stdin</span>是可读流,因此我们可以将其pipe到子进程<span style="background-color: rgba(192, 192, 192, 1)">stdin</span>流中。例如:</p>
<div class="cnblogs_code">
<pre>const { spawn } = require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
const child </span>= spawn('wc'<span style="color: rgba(0, 0, 0, 1)">);
process.stdin.pipe(child.stdin)
child.stdout.on(</span>'data', (data) =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(`child stdout:\n${data}`);
});</span></pre>
</div>
<p> 在上面的示例中,子进程调用了<span style="background-color: rgba(192, 192, 192, 1)">wc</span>命令,在Linux中该命令用来计算行数、单词数和字符数。然后,我们将主进程<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>(可读流)pipe到子进程<span style="background-color: rgba(192, 192, 192, 1)">stdin</span>(可写流)中。通过这样的操作,我们可以得到一种标准的输入模式,我们可以输入内容然后通过<span style="background-color: rgba(192, 192, 192, 1)">Ctrl+D</span>将内容传递给<span style="background-color: rgba(192, 192, 192, 1)">wc</span>命令。</p>
<p><img src="https://img2023.cnblogs.com/blog/51946/202310/51946-20231031165029848-1156843625.gif" alt="" style="display: block; margin-left: auto; margin-right: auto"></p>
<p> </p>
<p> 我们也可以在多个进程的标准输入/输出流之间使用pipe函数,就像我们在Linux命令行中所使用的那样。例如,我们可以将<span style="background-color: rgba(192, 192, 192, 1)">find</span>命令的<span style="background-color: rgba(192, 192, 192, 1)">stdout</span>通过pipe传给<span style="background-color: rgba(192, 192, 192, 1)">wc</span>命令的stdin,用来对当前目录中的文件进行计数:</p>
<div class="cnblogs_code">
<pre>const { spawn } = require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
const find </span>= spawn('find', ['.', '-type', 'f'<span style="color: rgba(0, 0, 0, 1)">]);
const wc </span>= spawn('wc', ['-l'<span style="color: rgba(0, 0, 0, 1)">]);
find.stdout.pipe(wc.stdin);
wc.stdout.on(</span>'data', (data) =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(`Number of files ${data}`);
});</span></pre>
</div>
<p> 我在<span style="background-color: rgba(192, 192, 192, 1)">wc</span>命令后面添加了<span style="background-color: rgba(192, 192, 192, 1)">-l</span>参数,确保只计算文件中内容的行数。运行之后,上述代码将对当前目录下的所有文件进行计数。</p>
<h3>Shell语法和exec函数</h3>
<p> 默认情况下,<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数不会创建一个<em>shell</em>来执行我们传给它的命令。这使得它比<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数效率更高,<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数会创建<em>shell</em>。与<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>相比,<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数还有另一个区别,它会缓冲命令的输出,并将整个结果传递给回调函数(而<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数则使用流来传递命令的结果)。</p>
<p> 下面是我们用<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数实现的前面<span style="background-color: rgba(192, 192, 192, 1)">find | wc</span>命令的例子。</p>
<div class="cnblogs_code">
<pre>const { exec } = require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
exec(</span>'find . -type f | wc -l', (err, stdout, stderr) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (err) {
console.error(`exec error: ${err}`);
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
}
console.log(`Number of files ${stdout}`);
});</span></pre>
</div>
<p> 因为<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数使用shell来执行命令,因此我们可以直接在其中使用<em>shell</em>语法<em>pipe</em>来连接两个命令。</p>
<p> 注意,如果我们使用从外部动态输入的字符串作为shell语法来执行命令,则可能会带来安全风险。用户可以简单地使用shell语法字符对命令进行注入攻击,例如<span style="background-color: rgba(192, 192, 192, 1)">command + '; rm -rf ~'</span>(这将会删除当前目录下的所有文件)。</p>
<p> <span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数缓冲命令的输出,并将其作为<span style="background-color: rgba(192, 192, 192, 1)">stdout</span>参数传递给回调函数(<span style="background-color: rgba(192, 192, 192, 1)">exec</span>的第二个参数),我们使用该参数打印命令的输出结果。</p>
<p> 如果你希望在命令中使用shell语法,并且命令返回的结果数据量很小,那么<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数是个不错的选择。(请记住,<span style="background-color: rgba(192, 192, 192, 1)">exec</span>在返回结果之前会将整个数据缓冲在内存中。)</p>
<p> 当命令返回的结果数据量比较大时,最好使用<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数,因为数据将与标准IO对象一起被传递。</p>
<p> 如果需要的话,我们可以使产生的子进程继承自父进程的标准IO对象,但更重要的是,我们也可以在<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数中使用shell语法。下面是在<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数中使用<span style="background-color: rgba(192, 192, 192, 1)">find | wc</span>命令:</p>
<div class="cnblogs_code">
<pre>const child = spawn('find . -type f | wc -l'<span style="color: rgba(0, 0, 0, 1)">, {
stdio: </span>'inherit'<span style="color: rgba(0, 0, 0, 1)">,
shell: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">
});</span></pre>
</div>
<p> 由于上面代码中的<span style="background-color: rgba(192, 192, 192, 1)">stdio: 'inherit'</span>选项,当我们运行时,子进程会继承主进程的<span style="background-color: rgba(192, 192, 192, 1)">stdin</span>,<span style="background-color: rgba(192, 192, 192, 1)">stdout</span>和<span style="background-color: rgba(192, 192, 192, 1)">stderr</span>。这将导致在主进程的<span style="background-color: rgba(192, 192, 192, 1)">stdout</span>流中触发子进程的数据事件处理程序,从而使脚本立即输出结果。</p>
<p> 另外由于上面代码中的<span style="background-color: rgba(192, 192, 192, 1)">shell: true</span>选项,我们可以在传入的命令中使用shell语法,就像我们在<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数中所做的那样。除此之外,我们仍然能够使用<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数数据流的优势。<em>这真是两全其美。</em></p>
<p><em> </em>除了<span style="background-color: rgba(192, 192, 192, 1)">shell</span>和<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>这两个选项之外,<span style="background-color: rgba(192, 192, 192, 1)">child_process</span>函数还有一些其它不错的选项。例如,我们可以使用<span style="background-color: rgba(192, 192, 192, 1)">cwd</span>选项来指定当前脚本的工作目录。下面这个例子使用<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数对我的用户目录下的Downloads文件夹中的所有文件进行计数。这里的<span style="background-color: rgba(192, 192, 192, 1)">cwd</span>选项指定脚本要计算的文件所在的目录为<span style="background-color: rgba(192, 192, 192, 1)">~/Downloads</span>:</p>
<div class="cnblogs_code">
<pre>const child = spawn('find . -type f | wc -l'<span style="color: rgba(0, 0, 0, 1)">, {
stdio: </span>'inherit'<span style="color: rgba(0, 0, 0, 1)">,
shell: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
cwd: </span>'/Users/samer/Downloads'<span style="color: rgba(0, 0, 0, 1)">
});</span></pre>
</div>
<p> 我们可以使用的另一个选项是<span style="background-color: rgba(192, 192, 192, 1)">env</span>,它用来为新产生的子进程指定可用的环境变量。此选项默认为<span style="background-color: rgba(192, 192, 192, 1)">process.env</span>,任何命令都可以访问当前进程的环境变量。如果想要改写环境变量的值,我们可以简单地将一个空对象传递给<span style="background-color: rgba(192, 192, 192, 1)">env</span>选项,或者指定一个新的值作为一个唯一的环境变量:</p>
<div class="cnblogs_code">
<pre>const child = spawn('echo $ANSWER'<span style="color: rgba(0, 0, 0, 1)">, {
stdio: </span>'inherit'<span style="color: rgba(0, 0, 0, 1)">,
shell: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
env: { ANSWER: </span>42<span style="color: rgba(0, 0, 0, 1)"> },
});</span></pre>
</div>
<p> 上面代码中的echo命令无法访问父进程的环境变量。例如,它无法访问<span style="background-color: rgba(192, 192, 192, 1)">$HOME</span>,但是它可以访问<span style="background-color: rgba(192, 192, 192, 1)">$ANSWER</span>,因为我们将<span style="background-color: rgba(192, 192, 192, 1)">$ANSWER</span>通过<span style="background-color: rgba(192, 192, 192, 1)">env</span>选项指定为自定义的环境变量。</p>
<p> 最后一个重要的选项是<span style="background-color: rgba(192, 192, 192, 1)">detached</span>,它使子进程独立于父进程运行。</p>
<p> 假设我们有一个<span style="background-color: rgba(192, 192, 192, 1)">timer.js</span>文件,它在事件循环中保持运行:</p>
<div class="cnblogs_code">
<pre>setTimeout(() =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> keep the event loop busy</span>
}, 20000);</pre>
</div>
<p> 我们可以使用<span style="background-color: rgba(192, 192, 192, 1)">detached</span>选项让它在后台运行:</p>
<div class="cnblogs_code">
<pre>const { spawn } = require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
const child </span>= spawn('node', ['timer.js'<span style="color: rgba(0, 0, 0, 1)">], {
detached: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
stdio: </span>'ignore'<span style="color: rgba(0, 0, 0, 1)">
});
child.unref();</span></pre>
</div>
<p> 从父进程分离子进程的具体行为取决于操作系统。在Windows中,分离的子进程具有自己独立的的控制台窗口,而在Linux中,分离的子进程会创建一个新的进程组和会话标签。</p>
<p> 如果在分离的子进程上调用了<span style="background-color: rgba(192, 192, 192, 1)">unref</span>函数,则父进程可以独立于子进程退出。如果子进程正在执行一个时间比较长的任务,这个功能会很有用。要让子进程在后台保持运行,我们还需要将子进程的<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>选项也配置为独立于父进程。</p>
<p> 上面的示例通过分离的子进程在后台执行一个Node脚本(<span style="background-color: rgba(192, 192, 192, 1)">timer.js</span>),并且忽略了父进程的<span style="background-color: rgba(192, 192, 192, 1)">stdio</span>文件描述符,这样当父进程被终止时,子进程仍然可以在后台保持运行。</p>
<p><img src="https://img2023.cnblogs.com/blog/51946/202310/51946-20231031171833364-1914336790.gif" alt="" style="display: block; margin-left: auto; margin-right: auto"></p>
<h3>execFile函数</h3>
<p> 如果你想在不使用shell的情况下执行一个文件,可以使用<span style="background-color: rgba(192, 192, 192, 1)">execFile</span>函数。它与<span style="background-color: rgba(192, 192, 192, 1)">exec</span>函数的行为完全相同,只是不使用shell,这使得<span style="background-color: rgba(192, 192, 192, 1)">execFile</span>函数的执行效率更高。在Windows中,某些文件无法单独执行,例如<span style="background-color: rgba(192, 192, 192, 1)">.bat</span>或<span style="background-color: rgba(192, 192, 192, 1)">.cmd</span>文件。这些文件不能使用<span style="background-color: rgba(192, 192, 192, 1)">execFile</span>函数执行,不过可以使用<span style="background-color: rgba(192, 192, 192, 1)">exec</span>或<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数并将shell选项设置为true来执行它们。</p>
<h3>*Sync函数</h3>
<p> <span style="background-color: rgba(192, 192, 192, 1)">child_process</span>模块中的<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>,<span style="background-color: rgba(192, 192, 192, 1)">exec</span>和<span style="background-color: rgba(192, 192, 192, 1)">execFile</span>函数都有对应的同步版本,当调用这些函数时,它会阻塞当前程序的执行直到子进程退出才会继续下一步。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">const {
spawnSync,
execSync,
execFileSync,
} </span>= require('child_process');</pre>
</div>
<p> 如果你试图简化脚本编写或者执行任何脚本任务,这些同步版本可能会很有用,但应该尽量避免使用它们。</p>
<h3>fork()函数</h3>
<p> <span style="background-color: rgba(192, 192, 192, 1)">fork</span>函数是<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>函数的变体,它用来产生一个node进程。<span style="background-color: rgba(192, 192, 192, 1)">spawn</span>和<span style="background-color: rgba(192, 192, 192, 1)">fork</span>之间的最大区别在于,当使用<span style="background-color: rgba(192, 192, 192, 1)">fork</span>时,子进程的通信信道会被建立,因此我们可以在子进程中使用<span style="background-color: rgba(192, 192, 192, 1)">send</span>函数与全局对象<span style="background-color: rgba(192, 192, 192, 1)">process</span>一起在父进程和子进程之间交换信息。我们通过<span style="background-color: rgba(192, 192, 192, 1)">EventEmitter</span>模块接口来实现这一操作。下面是具体的例子:</p>
<p>文件parent.js:</p>
<div class="cnblogs_code">
<pre>const { fork } = require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
const forked </span>= fork('child.js'<span style="color: rgba(0, 0, 0, 1)">);
forked.on(</span>'message', (msg) =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'Message from child'<span style="color: rgba(0, 0, 0, 1)">, msg);
});
forked.send({ hello: </span>'world' });</pre>
</div>
<p>文件child.js:</p>
<div class="cnblogs_code">
<pre>process.on('message', (msg) =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'Message from parent:'<span style="color: rgba(0, 0, 0, 1)">, msg);
});
let counter </span>= 0<span style="color: rgba(0, 0, 0, 1)">;
setInterval(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
process.send({ counter: counter</span>++<span style="color: rgba(0, 0, 0, 1)"> });
}, </span>1000);</pre>
</div>
<p> 在上面的<span style="background-color: rgba(192, 192, 192, 1)">parent.js</span>文件中,我们fork了<span style="background-color: rgba(192, 192, 192, 1)">child.js</span>(这将使用<span style="background-color: rgba(192, 192, 192, 1)">node</span>命令执行该文件),然后监听<span style="background-color: rgba(192, 192, 192, 1)">message</span>事件。每当<span style="background-color: rgba(192, 192, 192, 1)">child.js</span>使用<span style="background-color: rgba(192, 192, 192, 1)">process.send</span>发送数据时,<span style="background-color: rgba(192, 192, 192, 1)">message</span>事件都会被触发。在<span style="background-color: rgba(192, 192, 192, 1)">child.js</span>中,每隔一秒都会调用<span style="background-color: rgba(192, 192, 192, 1)">process.send</span>方法。</p>
<p> 要将消息从parent传递给child,我们可以在对象forked上执行<span style="background-color: rgba(192, 192, 192, 1)">send</span>函数,然后在<span style="background-color: rgba(192, 192, 192, 1)">child.js</span>中,我们监听全局对象<span style="background-color: rgba(192, 192, 192, 1)">process</span>的<span style="background-color: rgba(192, 192, 192, 1)">message</span>事件。</p>
<p> 当执行上面程序中的<span style="background-color: rgba(192, 192, 192, 1)">parent.js</span>文件时,它首先向下发送<span style="background-color: rgba(192, 192, 192, 1)">{ hello: 'world' }</span>对象,forked的子进程打印一个消息(Message from parent: { hello: 'world' }),然后<span style="background-color: rgba(192, 192, 192, 1)">child.js</span>将每秒发送一个递增的数值给父进程打印消息(Message from child{ counter: 0 })。</p>
<p><img src="https://img2023.cnblogs.com/blog/51946/202310/51946-20231031172829470-1309599998.gif" alt="" style="display: block; margin-left: auto; margin-right: auto"></p>
<p> 下面让我们来一个有关fork函数的更实际的例子。</p>
<p> 假设我们有一个http服务器,用来处理两个endpoint。其中一个endpoint(<span style="background-color: rgba(192, 192, 192, 1)">/compute</span>)耗时较长,它需要几秒钟才能响应。我们可以使用一个长的for循环来模拟它:</p>
<div class="cnblogs_code">
<pre>const http = require('http'<span style="color: rgba(0, 0, 0, 1)">);
const longComputation </span>= () =><span style="color: rgba(0, 0, 0, 1)"> {
let sum </span>= 0<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < 1e9; i++<span style="color: rgba(0, 0, 0, 1)">) {
sum </span>+=<span style="color: rgba(0, 0, 0, 1)"> i;
};
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> sum;
};
const server </span>=<span style="color: rgba(0, 0, 0, 1)"> http.createServer();
server.on(</span>'request', (req, res) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (req.url === '/compute'<span style="color: rgba(0, 0, 0, 1)">) {
const sum </span>=<span style="color: rgba(0, 0, 0, 1)"> longComputation();
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> res.end(`Sum is ${sum}`);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
res.end(</span>'Ok'<span style="color: rgba(0, 0, 0, 1)">)
}
});
server.listen(</span>3000);</pre>
</div>
<p> 这个程序有一个很大的问题。当请求<span style="background-color: rgba(192, 192, 192, 1)">/compute</span>时,由于事件循环忙于处理那个长的for循环操作,因此服务器将无法处理其它的请求。</p>
<p> 有几种方法可以解决此问题,不过有一种解决办法适用于所有的操作,我们可以使用<span style="background-color: rgba(192, 192, 192, 1)">fork</span>将计算移至另一个进程中。</p>
<p> 首先,我们将整个<span style="background-color: rgba(192, 192, 192, 1)">longComputation</span>函数移至一个新的文件中,并通过主进程的消息来调用该函数:</p>
<p>文件computer.js:</p>
<div class="cnblogs_code">
<pre>const longComputation = () =><span style="color: rgba(0, 0, 0, 1)"> {
let sum </span>= 0<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < 1e9; i++<span style="color: rgba(0, 0, 0, 1)">) {
sum </span>+=<span style="color: rgba(0, 0, 0, 1)"> i;
};
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> sum;
};
process.on(</span>'message', (msg) =><span style="color: rgba(0, 0, 0, 1)"> {
const sum </span>=<span style="color: rgba(0, 0, 0, 1)"> longComputation();
process.send(sum);
});</span></pre>
</div>
<p> 现在,我们不需要在主进程的事件循环中进行一个很费时的操作,我们可以<span style="background-color: rgba(192, 192, 192, 1)">fork</span>文件<span style="background-color: rgba(192, 192, 192, 1)">compute.js</span>,并使用message接口在服务器和fork的进程之间传递消息。</p>
<div class="cnblogs_code">
<pre>const http = require('http'<span style="color: rgba(0, 0, 0, 1)">);
const { fork } </span>= require('child_process'<span style="color: rgba(0, 0, 0, 1)">);
const server </span>=<span style="color: rgba(0, 0, 0, 1)"> http.createServer();
server.on(</span>'request', (req, res) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (req.url === '/compute'<span style="color: rgba(0, 0, 0, 1)">) {
const compute </span>= fork('compute.js'<span style="color: rgba(0, 0, 0, 1)">);
compute.send(</span>'start'<span style="color: rgba(0, 0, 0, 1)">);
compute.on(</span>'message', sum =><span style="color: rgba(0, 0, 0, 1)"> {
res.end(`Sum is ${sum}`);
});
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
res.end(</span>'Ok'<span style="color: rgba(0, 0, 0, 1)">)
}
});
server.listen(</span>3000);</pre>
</div>
<p> 当使用上述代码请求<span style="background-color: rgba(192, 192, 192, 1)">/compute</span>时,我们只需要简单地向fork的进程发送一条消息即可执行那个很费时的操作,而主进程的事件循环不会被阻塞。</p>
<p> 一旦fork的进程完成了那个很费时的操作,它可以通过<span style="background-color: rgba(192, 192, 192, 1)">process.send</span>将结果发送给主进程。</p>
<p> 在父进程中,我们监听fork的进程的<span style="background-color: rgba(192, 192, 192, 1)">message</span>事件。当该事件被触发后,我们获取到<span style="background-color: rgba(192, 192, 192, 1)">sum</span>值,然后通过http返回给请求者。</p>
<p> 当然,上面的代码中,我们可以fork的进程的数量是受限制的,但是当我们执行它并通过http请求一个耗时较长的endpoint时,主服务器不会被阻塞从而可以继续响应其它的请求。</p>
<p>原文地址:Node.js Child Processes: Everything you need to know</p><br><br>
来源:https://www.cnblogs.com/jaxu/p/17800599.html
頁:
[1]