关于NodeJS的子进程

1.什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

进程的概念主要有两点:

  • 进程是一个实体,每一个进程都有自己的地址空间。
  • 进程是一个"执行中的程序",存在嵌套关系。

我们可以通过下面的命令来查看当前电脑启动了多个进程:

  • 查看所有进程 tasklist(window)
  • 查看某个进程 tasklist | findstr "进程名"(window)

进程的嵌套关系大致如下:
在这里插入图片描述

  • 首先是操作系统进程OS
  • 其次是启动进程,所有的进程都是由sbin/launchd来启动的,这个其实就是一个桌面进程
  • 我们打开了 webstorm/vscode 的编辑器,就相当于开启了一个webstorm/vscode的进程
  • 在编辑器进程中启动了node应用,所以需要有个node进程
  • 在node进程中利用child_process创建nodejs的子进程

2.为什么需要子进程

Node.js中的非阻塞单线程的特性对单进程任务是非常有用。但是事实上,面对日益复杂的业务逻辑,单个cpu中的单进程所能提供的计算力显然是不足的。因为无论服务器如何强大,单线程只可以利用有限的资源。

子进程是核心模块,允许用户创建和控制子进程。这些进程可以执行系统命令、运行各种语言的脚本,甚至可以创建新的 Node.js 实例。child 进程模块的主要目的是允许同时执行多个进程,而不会阻塞主事件循环。

对于需要处理 CPU 密集型操作或执行外部命令和脚本的应用来说,使用 child 进程可以确保应用的高性能和响应性。

3.子进程的使用方式

child 进程可以用于以下任务:

  • 并行处理:Child 处理通过允许应用程序将工作负载分布在多个CPU核心上,从而显著提高 CPU 密集型活动(如图像处理和数据分析)的性能。
  • 运行 shell 脚本:Child 进程可以用来执行 shell 脚本。您可以使用 exec 技术来运行 shell 命令并捕获它们的输出,还可以使用 spawn 方法,当直接运行脚本时提供更大的控制。
  • 与其他服务通信:在通信方面,child 进程起着至关重要的作用。它可以与外部服务(如数据库、API 或微服务)进行通信。它们可以用来调用外部 API,执行数据库查询,与其它微服务通信。

4.子进程的创建方式

要创建一个 child 进程,Node.js 为我们提供了四种主要方法来创建一个 child 进程,分别是 exec(), execFile(), spawn(), fork()。

4.1 exec():执行shell脚本命令

exec() 方法在 shell 中运行一个命令并缓冲输出。它适用于运行 shell 命令并记录输出。但由于是缓冲,它有内存限制。

下面是一个使用 exec() 方法执行命令并获取和处理一些系统信息的例子。

const  {
    
    exec} = require('child_process');
const iconv= require('iconv-lite');//需要用这个转码,不然会乱码

// 执行命令时将 encoding 设为 'buffer'
exec('dir', {
    
     encoding: 'buffer' }, (error, stdout, stderr) => {
    
    
    if (error) {
    
    
        console.error(`执行命令出错: ${
      
      error}`);
        return;
    }

    // 假定 Windows 系统使用 CP936 编码(常见于中文系统)
    const output = iconv.decode(stdout, 'cp936');
    console.log('命令输出:', output);
});

在这里插入图片描述

4.2 execFile():执行shell脚本文件

execFile 用来执行一个shell脚本文件。

// shell脚本: test.shell
ls -al | grep package.json

echo $1
const {
    
     execFile } = require('child_process');
const path = require('path');

execFile(path.resolve(__dirname, 'test.shell'), [], (error, stdout, stderr) => {
    
    
    if (error) {
    
    
        console.error(`execFile error: ${
      
      error.message}`);
        return;
    }
    if (stderr) {
    
    
        console.error(`stderr: ${
      
      stderr}`);
        return;
    }
    console.log(`stdout: ${
      
      stdout}`);
});

execFile() 方法在没有 shell 的情况下运行可执行文件。它比 exec() 更高效,因为它避免了 shell 的开销。

不过execFile也能执行命令,不过配置必须放在第二个参数中,第一个参数只能是命令。

const {
    
     execFile } = require('child_process');
execFile("node", ["--version"], (error, stdout, stderr) => {
    
    
    if (error) {
    
    
        console.error(`execFile error: ${
      
      error.message}`);
        return;
    }
    if (stderr) {
    
    
        console.error(`stderr: ${
      
      stderr}`);
        return;
    }
    console.log(`stdout: ${
      
      stdout}`);
});

在这里插入图片描述

4.3 spawn() 方法

与主要用于执行 shell 命令或可执行文件并捕获其输出的 exec 方法不同,spawn() 方法通过基于流的输出处理、直接与可执行文件交互、事件驱动的通信,提供了对 child 进程的更多控制。

spawn() 方法可用于需要直接交互的复杂场景。spawn() 启动一个新进程,给定一个命令,并提供流 i.e stdout, stderr 用于直接处理进程的输出和错误。

下面是一个如何使用 spawn() 方法的例子

const {
    
     spawn } = require("child_process");
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const npmCommand = spawn(npm, ["-v"]);
npmCommand.stdout.on("data", (data) => {
    
    
    console.log(`stdout: ${
      
      data}`);
});
npmCommand.stderr.on("data", (data) => {
    
    
    console.error(`stderr: ${
      
      data}`);
});
npmCommand.on("close", (code) => {
    
    
    console.log(`child process exited with code ${
      
      code}`);
});

在这里插入图片描述

4.4 fork() 方法

在javascript中,在处理大量计算的任务方面,HTML里面通过web work来实现,使得任务脱离了主线程。在node中使用了一种内置于父进程和子进程之间的通信来处理该问题,降低了大数据运行的压力。node中提供了fork方法,通过fork方法在单独的进程中执行node程序,并且通过父子间的通信,子进程接受父进程的信息,并将执行后的结果返回给父进程。

  • 在子进程中:通过process.on(‘message’)和process.send()的机制来接收和发送消息。
  • 在父进程中:通过child.on(‘message’)和process.send()的机制来接收和发送消息。

适用场景:并行执行 JavaScript 文件,父子进程间通过消息通信。

//index.js
const {
    
     fork } = require('child_process');
const path  = require('path');
const child = fork(path.resolve(__dirname, 'child.js'));
child.on('message', message => {
    
    
    console.log(`来自子进程的消息: ${
      
      message}`);
});

child.send("child");
setInterval(() => {
    
    
    child.send("child");
}, 1000);
//child.js
process.on('message',function(msg){
    
    
    console.log("来自父进程的消息:", msg);
})
process.send("parent");

setInterval(() => {
    
    
    process.send("parent");
}, 1000);

在这里插入图片描述
使用 fork() 时,每个子进程都是一个独立的 Node.js 实例,具有独立的 V8 实例和事件循环。这意味着创建大量子进程可能会导致大量的资源消耗。

4.5 总结

选择合适的子进程创建方法取决于你的需求。如果你只需执行简单的 shell 命令,使用exec。对于可执行文件,使用execFile。如果需要并行执行 JavaScript 文件且支持消息通信,使用fork。而spawn适用于执行复杂命令并进行流式处理。根据不同的情况,选择最合适的方法可以提高代码的效率和可读性。

方法 区别 适用场景
exec 执行 shell 命令,创建 shell进程 执行简单命令,获取输出和错误信息
execFile 执行可执行文件 执行编译好的二进制文件,获取输出和错误信息
fork 衍生 Node.js 子进程,执行 JS 文件 创建独立的子进程并与父进程通过消息通信
spawn 执行命令,不创建 shell 进程 执行复杂命令,流式处理输入输出