在NodeJS中,只支持单线程。但是在应用程序中,如果只使用单线程进行操作,从接收请求开始到返回响应为止的这一段时间里可能存在很长的一段等待时间。在这种场合下,如果能够使用多进程,则可以为每个请求分配一个进程,从而可以更好地使用服务器端的CPU资源。为了实现多进程处理,NodeJS中提供了child_process模块和cluster模块

  • child_process模块:用于实现在NodeJS应用程序中开启多个子进程并在各个子进程中运行各种不同的命令或执行NodeJS模块文件、可执行文件的处理。
  • cluster模块:用于实现在NodeJS应用程序中开启多个子进程,在每个子进程中运行一个NodeJS应用程序副本的处理。

NodeJS中的进程(process对象)

在操作系统中,每个应用程序都是一个进程类的实例对象。

进程对象属性

  • execPath:属性值为用来运行应用程序的可执行文件的绝对路径。
  • version:属性值为NodeJS的版本号。
  • versions:属性值为NodeJS及其各依赖的版本号。
  • platform:属性值为当前运行NodeJS的平台。
  • argv:属性值为运行NodeJS应用程序时的所有命令行参数。
  • stdin:属性值为一个用于读入标准输入流的对象。
  • stdout:属性值为一个用于写入标准输出流的对象。
  • stderr:属性值为一个用于写入标准错误输出流的对象。
  • pid:属性值为运行当前NodeJS应用程序的进程的PID。
  • arch:属性值运行NodeJS应用程序的处理器架构。
> process.title
'node'
> process.version
'v8.16.0'
> process.versions
{ http_parser: '2.8.0',
  node: '8.16.0',
  v8: '6.2.414.77',
  uv: '1.23.2',
  zlib: '1.2.11',
  ares: '1.10.1-DEV',
  modules: '57',
  nghttp2: '1.33.0',
  napi: '4',
  openssl: '1.0.2r',
  icu: '60.1',
  unicode: '10.0',
  cldr: '32.0',
  tz: '2017c' }
> process.execPath
'/Users/liujie26/.nvm/versions/node/v8.16.0/bin/node'
> process.platform
'darwin'
> process.pid
53777
> process.arch
'x64'
> process.argv
[ '/Users/liujie26/.nvm/versions/node/v8.16.0/bin/node' ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 默认情况下,标准输入流处于暂停状态,需要使用resume方法恢复读取标准输入流数据
process.stdin.resume();
process.stdin.on('data', (chunk) => {
    process.stdout.write('进程接收到数据:' + chunk);
});
// 测试结果:
123
进程接收到数据:123
234
进程接收到数据:234
1
2
3
4
5
6
7
8
9
10
process.argv.forEach((val, index, array) => {
    console.log(index + ':' + val);
});
// 测试结果:
0:/usr/local/bin/node
1:/Users/liujie/study/node/part9/demo2.js
2:one
3:two
4:three
1
2
3
4
5
6
7
8
9

进程对象的方法与事件

  • memoryUsage方法:获取运行NodeJS应用程序的进程的内存使用量。
    • rss:属性值为整数,表示运行NodeJS应用程序的进程的内存消耗量,单位为字节
    • heapTotal:属性值为整数,表示为V8所分配的内存量,单位为字节
    • 属性值为整数,表示为V8的内存消耗量,单位为字节
  • nextTick方法:用于将一个函数推迟到代码中所书写的下一个同步方法执行完毕时或异步方法的事件回调函数开始执行时调用。

nextTick方法的作用与将setTimeout方法的时间参数值指定为0的作用相同。

  • chdir方法:用于修改NodeJS应用程序中使用的当前工作目录。
  • cwd方法:用于返回当前目录。
  • exit方法:用于退出运行NodeJS应用程序的进程。
  • kill方法:用于向一个进程发送信号。
  • umask方法:用于读取或修改运行NodeJS应用程序的进程的文件权限掩码。子进程将继承父进程的文件权限掩码。
  • uptime方法:返回NodeJS应用程序的当前运行时间
  • hrtime方法:用于测试一个代码段的运行时间
> process.memoryUsage()
{ rss: 25419776,
  heapTotal: 7684096,
  heapUsed: 5187696,
  external: 8627 }

1
2
3
4
5
6
console.log('当前目录:' + process.cwd());
process.chdir('../');
console.log('上层目录:' + process.cwd());
1
2
3
var oldmask, newmask = 0644;
oldmask = process.umask(newmask);
console.log('修改前的掩码:' + oldmask.toString(8) + ',修改后的掩码:' + newmask.toString(8));
1
2
3
const fs = require('fs');
const time = process.hrtime();
const data = fs.readFileSync('./demo.js');
const diff = process.hrtime(time);
// hrtime方法的返回值diff是一个数组
// 数组中包含两个时间,第一个时间单位为秒,第二个时间单位为纳秒
console.log(diff); // [ 0, 516897 ]
console.log('读文件操作耗费%d纳秒', diff[0] * 1e9 + diff[1]); // 读文件操作耗费516897纳秒
1
2
3
4
5
6
7
8
  • exit事件:当运行的NodeJS应用程序的进程退出时触发进程对象的exit事件。

创建多进程应用程序

进程创建有多种方式,这里主要介绍child_process模块和cluster模块。

child_process模块

  • child_process.spawn():适用于返回大量数据,例如图像处理,二进制数据处理。
  • child_process.exec():适用于小量数据,maxBuffer 默认值为 200 * 1024 超出这个默认值将会导致程序崩溃,数据量过大可采用 spawn。
  • child_process.execFile():类似 child_process.exec(),区别是不能通过 shell 来执行,不支持像 I/O 重定向和文件查找这样的行为。
  • child_process.fork(): 衍生新的进程,进程之间是相互独立的,每个进程都有自己的 V8 实例、内存,系统资源是有限的,不建议衍生太多的子进程出来,通长根据系统** CPU 核心数**设置。

使用spawn方法开启子进程

在child_process模块中,spawn方法用于开启一个用于运行某个命令的子进程。该方法返回一个隐式创建的代表子进程的ChildProcess对象。

使用fork方法开启子进程

在child_process模块中,fork方法用于开启一个专用于运行NodeJS中某个模块文件的子进程。该方法返回一个隐式创建的代表子进程的ChildProcess对象。

const cp = require('child_process');
const n = cp.fork(__dirname + '/test.js');
// 当父进程接收到子进程中发送的消息时,触发子进程对象的message事件
n.on('message', msg => {
    console.log('父进程接收到消息:', msg);
    process.exit();
});
// 在父进程中使用子进程对象的send方法向子进程发送消息
n.send({ usernName: '我是父进程' });
1
2
3
4
5
6
7
8
9
// test.js
// 当子进程对象接收到消息后,触发process对象的message事件
process.on('message', (msg) => {
    console.log('子进程接收到消息:', msg);
    // 在子进程中使用进程对象的send方法向父进程发送消息
    process.send({ name: '我是子进程' });
});
1
2
3
4
5
6
7
node demo.js
// 结果如下:
子进程接收到消息: { usernName: '我是父进程' }
父进程接收到消息: { name: '我是子进程' }
1
2
3
4

需要注意:

  • 在使用fork方法时,当子进程中所有输入/输出操作执行完毕后,子进程不会自动退出。必须使用**process.exit()**方法将其显式退出。

使用exec方法开启子进程

在child_process模块中,exec方法用于开启一个用于运行某个命令的子进程并缓存子进程中的输出结果。该方法返回一个隐式创建的代表子进程的ChildProcess对象。

使用execFile方法开启子进程

在child_process模块中,execFile方法用于开启一个专用于运行某个可执行文件的子进程。该方法返回一个隐式创建的代表子进程的ChildProcess对象。

在多个子进程中运行NodeJs应用程序

进程创建demo

const http = require('http');

const longComputation = () => {
    let sum = 0;
    for (let i = 0; i < 1e10; i++) {
        sum += i;
    }
    return sum;
}

const server = http.createServer();
server.on('request', (req, res) => {
    if (req.url === '/compute') {
        const sum = longComputation();
        return res.end(`Sum is ${sum}`);
    } else {
        res.end('ok');
    }
});

server.listen(8080, () => {
    console.log('server is running at port 8080');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行上述代码,然后打开两个浏览器tab页分别访问http://localhost:8080/computehttp://localhost:8080/,可以发现服务器接收到/compute请求时会进行大量的数值计算,导致无法响应其他的请求(/)。

在Java语言中可以通过多线程的方式来解决上述的问题,但是Node.js在代码执行的时候是单线程的,那么Node.js应该如何解决上面的问题呢?其实,在Node.js中可以通过child_process模块创建一个子进程执行密集的CPU计算任务(例如上面例子中的longComputation)来解决上述问题。具体代码如下所示:

// fork_app.js
const http = require('http');
const fork = require('child_process').fork;

const server = http.createServer();
server.on('request', (req, res) => {
    if (req.url === '/compute') {
        const compute = fork('./fork_compute.js');
        compute.send('开启子进程了');

        // 子进程使用process.send()发送消息时会触发message事件
        compute.on('message', data => {
            res.end(`Sum is ${data}`);
            compute.kill();
        });
        // 子进程监听到错误消息时退出
        compute.on('close', (code, signal) => {
            console.log(`触发了close事件,子进程收到信号${signal}终止了,退出码为${code}`);
            compute.kill();
        });
    } else {
        res.end('ok');
    }
});

server.listen(8080, () => {
    console.log('server is running at port 8080');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// fork_compute.js
const longComputation = () => {
    let sum = 0;
    console.info('计算开始了!!!');
    console.time('计算耗时');
    for (let i = 0; i < 1e10; i++) {
        sum += i;
    }
    console.info('计算结束了!!!');
    console.timeEnd('计算耗时');
    return sum;
}

process.on('message', msg => {
    console.log(msg, 'process.pid', process.pid); // 输出子进程id
    const sum = longComputation();
    // 如果Node.js进程是通过进程间通信产生的,那么,process.send()方法可以用来给父进程发送消息
    process.send(sum);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
开启子进程了 process.pid 80554
计算开始了!!!
计算结束了!!!
计算耗时: 11917.019ms
触发了close事件,子进程收到信号SIGTERM终止了,退出码为null
1
2
3
4
5

进程间通信原理

进程守护

参考文档

  1. Node.js child_process模块解读
  2. Node.js 中文网