Node.js中的单线程无阻塞性能非常适合单个进程。但是,一个CPU中的一个进程不足以处理应用程序不断增加的工作量。无论您的服务器多么强大,一个线程只能支持有限的负载。Node.js在单个线程中运行的事实,然而我们可以利用多进程。

多个进程是扩展Node应用程序的最佳方法。 Node.js设计用于构建具有多个节点的分布式应用程序。这就是为什么它被命名节点。 可伸缩性已植入平台中,您在应用程序的生命周期后期就不会开始考虑它。

子进程模块

Node的child_process模块轻松创建一个子进程,并且这些子进程可以通过消息传递系统互相通信。
child_process模块使我们能够通过在子进程中运行任何系统命令来访问操作系统功能。
我们可以控制该子进程的输入流,并监听其输出流。还可以控制要传递给底层OS命令的参数,并且可以对该命令的输出执行任何所需的操作。
例如,可以将一个命令的输出作为输入传递给另一个命令(就像我们在Linux中一样),因为可以使用Node.js流向我们展示这些命令的所有输入和输出。
有四种不同的方法来创建节点的子流程:

  • spawn
  • fork
  • exec
  • execfile

child_pr.png

spawn

spawn函数在新进程中启动命令,可以使用它传递该命令的任何参数。如下:

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

只需将spawn功能从child_process模块中解构出来,然后以OS命令作为第一个参数执行即可。
执行该spawn函数(child上面的对象)的结果是一个ChildProcess实例,该实例实现了EventEmitter API。这意味着我们可以直接在此子对象上注册事件的处理程序。例如,当子进程退出时,我们可以通过为exit事件注册一个处理程序来做一些事情。

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

上面的处理程序为我们code提供了子进程的出口以及signal用于终止子进程的(如果有的话)。signal当子进程正常退出时,此变量为null。
用注册的处理程序ChildProcess实例是disconnect,error,close,和message。

  • disconnect当父进程手动调用该child.disconnect函数时,将触发该事件。
  • error如果无法生成或杀死进程,则触发该事件。
  • close当stdio子进程的流关闭时,将触发该事件。
  • message事件是最重要的事件。当子进程使用该process.send()函数发送消息时发出。父/子进程可以这样相互通信。我们将在下面看到一个示例。

每个子进程也得到了三个标准的stdio流,可以访问 child.stdin、child.stdout 和 child.stderr。
当这些流关闭时,使用它们的子进程将发出close事件。close事件与exit事件不同,因为多个子进程可能共享相同的stdio流,因此一个子进程退出并不意味着流被关闭。

由于所有流都是事件发射器,因此我们可以在stdio附加到每个子进程的流上侦听不同的事件。与普通进程不同,在子进程中,stdout/ stderr流是可读流,而stdin流是可写流。从本质上讲,这与在主要过程中发现的那些类型相反。我们可以用于这些流的事件是标准事件。最重要的是,在可读流上,我们可以监听data事件,该事件将包含命令的输出或执行命令时遇到的任何错误:

  1. child.stdout.on('data', (data) => {
  2. console.log(`child stdout:\n${data}`);
  3. });
  4. child.stderr.on('data', (data) => {
  5. console.error(`child stderr:\n${data}`);
  6. });

上面的两个处理程序会将这两种情况都记录到主进程stdout和中stderr。当我们执行spawn上面的函数时,pwd命令的输出将被打印,并且子进程将使用code退出0,这意味着未发生任何错误。

可以spawn使用函数的第二个参数spawn将参数传递给函数执行的命令,该参数是要传递给命令的所有参数的数组。例如,要find【linux系统的命令】在带有-type f参数的当前目录上执行命令(仅列出文件),我们可以执行以下操作:

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

如果在命令执行过程中发生错误,例如,如果上面给出查找无效的目标,则child.stderr data事件处理程序将被触发,exit事件处理程序将报告退出代码1,表示发生错误。错误值实际上取决于主机操作系统和错误类型。

子进程stdin是可写流。我们可以使用它向命令发送一些输入。就像任何可写流一样,使用它的最简单方法是使用pipe函数。我们只是将可读流传输到可写流。由于主流程 process.stdin 是可读流,因此我们可以将其通过管道stdin传输到子流程child.stdin流。例如:

  1. const { spawn } = require('child_process');
  2. const child = spawn('wc');
  3. process.stdin.pipe(child.stdin)
  4. child.stdout.on('data', (data) => {
  5. console.log(`child stdout:\n${data}`);
  6. });

在上面的示例中,子进程调用wc命令,该命令在Linux中对行,单词和字符进行计数。
wc命令在linux中对行,单词和字符进行计数。

  1. [“-l”]表示只对行统计,
  2. [“-c”]表示只显示bytes统计,
  3. [“-w”]表示只对字数统计,

然后,我们将主进程stdin(这是可读流)通过管道传送到子进程stdin(这是可写流)。这种结合的结果是我们得到了一个标准的输入模式,在该模式下,我们可以键入一些内容,当我们点击时Ctrl+D,所键入的内容将用作wc命令的输入。
1_s9dQY9GdgkkIf9zC1BL6Bg.gif我们也可以通过管道将多个进程的标准输入/输出相互传递,就像我们可以使用Linux命令一样。例如,我们可以stdout将find命令的管道传输到命令的标准输入,wc以计算当前目录中的所有文件:

  1. const { spawn } = require('child_process');
  2. const find = spawn('find', ['.', '-type', 'f']);
  3. const wc = spawn('wc', ['-l']);
  4. find.stdout.pipe(wc.stdin);
  5. wc.stdout.on('data', (data) => {
  6. console.log(`Number of files ${data}`);
  7. });

我增加了一个 -l 参数,以使其仅计算行数。执行后,以上代码将输出当前目录下所有目录中所有文件的计数。

Shell语法和exec函数

默认情况下,该spawn函数不会创建 shell 程序来执行我们传递给它的命令。这使它比exec创建外壳的函数稍微更有效。该exec功能还有另一个主要区别。它缓冲命令生成的输出,并将整个输出值传递给回调函数(而不是使用流,像这样spawn做)。
这是find | wc 使用exec函数实现的先前示例。

  1. const { exec } = require('child_process');
  2. exec('find . -type f | wc -l', (err, stdout, stderr) => {
  3. if (err) {
  4. console.error(`exec error: ${err}`);
  5. return;
  6. }
  7. console.log(`Number of files ${stdout}`);
  8. });

由于该exec函数使用shell执行命令,因此我们可以在此处利用 shell 管道功能直接使用shell 语法
请注意,如果您执行外部提供的任何类型的动态输入,则使用Shell语法会带来安全风险。用户可以简单地使用shell语法字符(例如;)进行命令注入攻击。和$(例如,command + ’; rm -rf ~’)
该exec函数缓冲输出,并将其exec作为stdout那里的参数传递给回调函数(的第二个参数)。此stdout参数是我们要打印的命令输出。
exec如果需要使用shell语法,并且命令期望的数据量很小,则该函数是不错的选择。(请记住,exec将在返回之前将整个数据缓冲在内存中。)

spawn 命令期望的数据量很大时,该函数是更好的选择,因为该数据将与标准IO对象一起作为流传输
如果需要,我们可以使产生的子进程继承其父进程的标准IO对象,而且更重要的是,我们还可以使spawn函数使用shell语法。这是find | wc使用spawn函数实现的相同命令:

  1. const child = spawn('find . -type f | wc -l', {
  2. stdio: 'inherit',
  3. shell: true
  4. });

因为参数设置stdio: ‘inherit’ 选项,当执行代码,子进程继承主要过程 stdin,stdout和stderr。这导致子进程数据事件处理程序在主进程上被触发process.stdout,从而使脚本立即输出结果。
由于上面 shell: true的选项,我们能够在传递的命令中使用shell语法,就像我们对所做的那样exec。但是使用此代码,我们仍然可以获得该spawn函数提供给我们的数据流的优势。这确实是两全其美。
child_process除了shell以外,我们还可以在函数的最后一个参数中使用其他一些不错的配置 stdio。例如,我们可以使用该cwd选项来更改脚本的工作目录。例如,这是spawn使用shell程序将工作目录设置为我的“下载”文件夹的全部文件示例。cwd这里的选项将使脚本计算~/Downloads:所有的所有文件

  1. const child = spawn('find . -type f | wc -l', {
  2. stdio: 'inherit',
  3. shell: true,
  4. cwd: '/Users/samer/Downloads'
  5. });

还可以使用的另一个选项: env 指定对新子进程可见的环境变量的选项。此选项默认值是process.env。使任何命令都可以访问当前进程环境。如果要覆盖该行为,我们可以简单地传递一个空对象作为env选项或将新值视为唯一的环境变量:

  1. const child = spawn('echo $ANSWER', {
  2. stdio: 'inherit',
  3. shell: true,
  4. env: { ANSWER: 42 },
  5. });

上面的echo命令无法访问父进程的环境变量。例如,$HOME它不能使用access ,但是它可以访问,$ANSWER因为它是通过该env选项作为自定义环境变量传递的。
最后一个要解释的重要子进程选项是 detached 选项,它使子进程独立于其父进程运行。
假设有一个timer.js 使事件延时处理,使得子进程延时结束。

  1. setTimeout(() => {
  2. // keep the event loop busy
  3. }, 20000);

默认情况下,父进程将等待分离的子进程退出,为了防止父进程等待给定的子进程退出,可以使用以下detached选项在后台执行它:

  1. const { spawn } = require('child_process');
  2. const child = spawn('node', ['timer.js'], {
  3. detached: true,
  4. stdio: 'ignore'
  5. });
  6. child.unref();

分离的子进程实际行为取决于操作系统。在Windows上,分离的子进程将具有其自己的控制台窗口,而在Linux上,分离的子进程将成为新进程组和会话的领导者。
如果在unref分离的进程上调用该函数,则父进程可以独立于子进程退出。如果子进程正在执行一个长时间运行的进程,这可能会很有用,但要使其在后台运行,子进程的stdio配置也必须独立于父进程。
上面的示例将timer.js通过分离并忽略其父stdio文件描述符在后台运行节Å点脚本(),从而允许父进程独立于子进程退出终止。
1_WhvMs8zv-WS6v7nDXmDUzw.gif

execFile函数

如果需要在不使用Shell的情况下执行文件,那么该execFile功能就是您所需要的。它的行为与该exec函数完全相同,但是不使用 shell 程序,因此使其效率更高。
child_process.exec()child_process.execFile() 之间区别的重要性可能因平台而异。 在 Unix 类型的操作系统(Unix、Linux、macOS)上,child_process.execFile() 可以更高效,因为它默认不衍生 shell。 但是,在 Windows 上,.bat 和 .cmd 文件在没有终端的情况下无法自行执行,因此无法使用 child_process.execFile() 启动。 在 Windows 上运行时,.bat 和 .cmd 文件可以使用具有 shell 选项集的 child_process.spawn()、使用 child_process.exec()、或通过衍生 cmd.exe 并将 .bat 或 .cmd 文件作为参数传入(这也是 shell 选项和 child_process.exec() 所做的)来调用。

  1. // 仅在 Windows 上...
  2. const { spawn } = require('child_process');
  3. const bat = spawn('cmd.exe', ['/c', 'my.bat']);
  4. bat.stdout.on('data', (data) => {
  5. console.log(data.toString());
  6. });
  7. bat.stderr.on('data', (data) => {
  8. console.error(data.toString());
  9. });
  10. bat.on('exit', (code) => {
  11. console.log(`Child exited with code ${code}`);
  12. });

exec函数和execFile区别

execFile和exec最大的区别是,execFIle默认不会创建新的shell。需要手动开启shell命令。

execFile()内部,options.shell === false

  1. var child_process = require('child_process');
  2. var execFile = child_process.execFile;
  3. var exec = child_process.exec;
  4. exec('ls -al .', function(error, stdout, stderr){
  5. if(error){
  6. throw error;
  7. }
  8. console.log(stdout);
  9. });
  10. // execFile手动设置shell,如果不设置会报错
  11. execFile('ls -al .', {shell: '/bin/bash'}, function(error, stdout, stderr){
  12. if(error){
  13. throw error;
  14. }
  15. console.log(stdout);
  16. });

execFile()内部最终还是通过spawn()实现的, 如果没有设置 {shell: ‘/bin/bash’},那么 spawm() 内部对命令的解析会有所不同,execFile(‘ls -al .’) 会直接报错

Sync功能

功能spawn,exec以及execFile从child_process模块还具有同步阻塞版本将等到子进程退出

  1. const {
  2. spawnSync,
  3. execSync,
  4. execFileSync,
  5. } = require('child_process');

当试图简化脚本编写任务或任何启动处理任务时,这些同步版本可能很有用,但应避免使用它们

fork() 函数

该fork功能是spawn用于生成节点进程的功能的变体。spawn和fork之间的最大区别,在使用时fork,send将为子进程建立一个通信通道,因此我们可以将子进程上的函数与全局process对象本身一起使用,以在父进程和子进程之间交换消息。我们通过EventEmitter模块接口来实现。这是一个例子:
父文件parent.js:

  1. const { fork } = require('child_process');
  2. const forked = fork('child.js');
  3. forked.on('message', (msg) => {
  4. console.log('Message from child', msg);
  5. });
  6. forked.send({ hello: 'world' });

child.js

  1. process.on('message', (msg) => {
  2. console.log('Message from parent:', msg);
  3. });
  4. let counter = 0;
  5. setInterval(() => {
  6. process.send({ counter: counter++ });
  7. }, 1000);

在上面的父文件中,我们进行分离 child.js(它将使用node命令执行该文件),然后侦听该message事件。message每当 child.js 使用时process.send,都会发出该事件,每秒都会这样做。
为了将消息从父对象传递给子对象,可以send在fork对象本身上执行该函数,然后,在子脚本中,我们可以在全局process对象上侦听message的事件。
当执行parent.js文件时,它将首先发送{ hello: ‘world’ }由子进程打印的对象,然后,子进程将每秒发送一个递增的计数器值,以供父进程打印。
1_GOIOTAZTcn40qZ3JwgsrNA.gif
让我们为该fork函数做一个更实际的例子。
假设我们有一个处理两个node的http服务器。这些node之一(/compute如下所示)在计算上很昂贵,将需要几秒钟来完成。我们可以使用long for循环来模拟:

  1. const http = require('http');
  2. const longComputation = () => {
  3. let sum = 0;
  4. for (let i = 0; i < 1e9; i++) {
  5. sum += i;
  6. };
  7. return sum;
  8. };
  9. const server = http.createServer();
  10. server.on('request', (req, res) => {
  11. if (req.url === '/compute') {
  12. const sum = longComputation();
  13. return res.end(`Sum is ${sum}`);
  14. } else {
  15. res.end('Ok')
  16. }
  17. });
  18. server.listen(3000);

这个程序有个大问题;当/compute请求端点时,服务器将无法处理任何其他请求,因为事件循环忙于long for循环操作。

根据长操作的性质,有几种方法可以解决此问题,但是对所有操作都有效的一种解决方案是,将计算操作移至另一个过程中fork
我们首先将整个longComputation函数移到其自己的文件中,并在通过主进程的消息指示时使其调用该函数:
在一个新compute.js文件中:

  1. const longComputation = () => {
  2. let sum = 0;
  3. for (let i = 0; i < 1e9; i++) {
  4. sum += i;
  5. };
  6. return sum;
  7. };
  8. process.on('message', (msg) => {
  9. const sum = longComputation();
  10. process.send(sum);
  11. });

现在,我们无需在主进程事件循环中进行冗长的操作,而是可以fork使用compute.js文件并使用messages接口在服务器和子进程之间传递消息。

  1. const http = require('http');
  2. const { fork } = require('child_process');
  3. const server = http.createServer();
  4. server.on('request', (req, res) => {
  5. if (req.url === '/compute') {
  6. const compute = fork('compute.js');
  7. compute.send('start');
  8. compute.on('message', sum => {
  9. res.end(`Sum is ${sum}`);
  10. });
  11. } else {
  12. res.end('Ok')
  13. }
  14. });
  15. server.listen(3000);

当/compute上面的代码立即请求执行时,我们仅向fork子进程发送一条消息即可开始执行长操作。主进程的事件循环将不会被阻止。
分叉处理完成该长时间的操作后,便可以使用将结果发送回父处理process.send。
在父进程中,我们message在派生子进程本身上侦听事件。收到该事件后,我们将sum准备好一个值,供我们通过http发送给发出请求的用户。
当然,上面的代码受我们可以fork的进程数量的限制,但是当我们执行它并通过http请求较长的计算端点时,主服务器根本不会被阻塞,并且可以接受其他请求。

node的子进程:https://www.cnblogs.com/chyingp/p/node-learning-guide-child_process.html
https://medium.com/edge-coders/node-js-child-processes-everything-you-need-to-know-e69498fe970a