Node 子进程

Node.js 采用单线程模型,这意味着 Node.js 只有一个线程来处理所有的请求。

然而,有些任务需要耗费大量的 CPU 时间或 IO 操作,这些任务可能会阻塞事件循环,导致整个应用程序变得缓慢或无响应。为了解决这个问题,Node.js 引入了一些方法来实现多线程处理。为了解决这个问题,Node.js 提供了 Cluster 模块和 Child_process 模块。

Child_process 模块

我们在 Child_process 中创建的进程都是 Node.js 主进程下嵌套的子进程。

  • 查看启动的进程:ps -ef|grep 84926

Child_process 模块可以在 Node.js 中启动子进程,并且可以利用多核 CPU 来处理计算密集型任务。

常用异步行数:

  • spawn():启动一个子进程,适合耗时操作,创建了管道,流

  • fork():启动一个新的 Node.js 进程

  • exec():可以执行一个 shell 命令,适合开销比较小的任务,数据一次性返回

  • execFile():可以执行一个可执行文件

同步函数:

  • execSync
  • execFileSync
  • spawnSync

spawn()

spawn 函数主要用于执行那些不需要终端交互的命令,它接收一个命令字符串和一个参数数组,将这个命令转换为一个子进程来执行。因为它不需要创建新的 V8 实例,所以它的启动速度很快,而且在处理大量的小型任务时非常高效。

execexecFile底层实现都是使用spawn

常见用法包括:

  • 运行外部耗时命令,列如,执行 git clone 或 npm install
  • 处理大型数据集,将任务拆分成多个子进程,以避免内存问题
  • 执行非常耗时的操作,而不会阻止事件循环

子进程对象会有三个事件:

  • stdout:可以通过监听该事件获取子进程输出的数据
  • stderr:可以通过监听该事件获取子进程输出的错误信息
  • close:可以通过监听该事件获取子进程的退出码

fork()

会启动一个 V8 引擎创建一个新的 Node.js 进程,并且在子进程中运行指定的模块文件。fork 函数返回一个 ChildProcess 对象,可以在父进程中使用该对象与子进程进行通信。

常见用法包括:

  • 处理 CPU 密集型任务:可以创建多个子进程,让它们分别运行相同或不同的模块文件,从而充分利用 CPU 资源
  • 在父进程中监听端口,将请求转发给子进程处理,以实现负载均衡
  • 启动多个子进程,并通过 IPC 共享状态,以提高程序的并发性能

spawn() 和 fork()

spawn 和 fork 的选择取决于使用场景和需求。一般来说,如果只需要启动一个独立的进程并与之通信,使用 spawn 即可;如果需要启动一个新的 Node.js 实例并与之共享状态和通信,使用 fork 更加方便。

Cluster 模块

Cluster 模块可以让 Node.js 在多个进程之间进行负载均衡,并且可以通过 IPC 机制实现进程之间的通信和状态共享。

Cluster 模块的使用场景通常是在需要处理高并发请求的场景下,比如 Web 应用程序、API 服务器等。

Worker Threads

它可以在 Node.js 中创建多个线程,并且可以通过共享内存来提高性能。

Cluster 适用于需要利用多核处理器的能力来提高应用程序的性能的场景,而 Worker Threads 则适用于在单个线程内并行执行 CPU 密集型任务的场景。

源码分析

  • exec 和 execFile 有什么区别?
  • 为什么 exec/execFile/fork 都是通过 spawn 实现?
  • 为什么 spawn 没有回调方法,而 exec 和 execFile 却有?
  • spawn 中 stdout 和 stderr 到底是什么?
  • 回调方法的执行顺序是怎样的?

Node 多进程开发

核心用法是创建子进程:child_process

进程

概念:

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

macOS系统中运行一个 Node.js 程序,操作系统就会创建一个进程,在 Node.js 中创建一个子进程,就会存在嵌套关系。

我们在cchild_process中创建的进程就是 Node.js 的子进程。

进程嵌套关系

image

常用方法

exec 适合开销比较小的任务

const cp = require('child_process');

// 执行shell命令
cp.exec('ls -al', (err, stdout, stderr) => {
  console.log(err);
  console.log(stdout);
  console.log(stderr);
});

// 执行shell文件,直接衍生为一个新进程
cp.execFile(
  path.resolve(__dirname, 'test.shell'),
  ['-al'],
  (err, stdout, stderr) => {
    console.log(err);
    console.log(stdout);
    console.log(stderr);
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

spawn 适合耗时任务,需要不断日志,比如: npm install

const child = cp.spawn(path.resolve(__dirname, 'test.shell'), ['-al', '-bl'], {
  pwd: path.resolve(__dirname),
});

child.stdout.on('data', (chunk) => {
  console.log(chunk.toString());
});
child.stderr.on('data', (chunk) => {
  console.log(chunk);
});
1
2
3
4
5
6
7
8
9
10

或者使用stdio: inherit

const child = cp.spawn('node', ['-e', code], {
  cwd: process.cwd(),
  stdio: 'inherit',
});

child.on('error', (e) => {
  log.error(e.message);
  process.exit(1);
});
child.on('exit', (e) => {
  if (e === 1) {
    log.error('命令执行失败:' + e);
  } else {
    log.verbose('命令执行成功:' + e);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

fork 会使用 Node 创建一个子进程来执行文件

cp.fork(path.resolve(__dirname, 'child.js'));
console.log(process.pid);
1
2

进程之间通信

// index.js
const child = cp.fork(path.resolve(__dirname, 'child.js'));
child.send('hello', () => {
  child.disconnect(); // 断开连接
});

// child.js 接收
process.on('message', (msg) => {
  console.log(msg);
});
1
2
3
4
5
6
7
8
9
10
Last Updated: 2023/8/2 10:45:34
Contributors: licong96