在介绍child_process模块以前,先来看一个下面的代码。html
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(3000);
能够试一下使用上面的代码启动Node.js服务,而后打开两个浏览器选项卡分别访问/compute和/,能够发现node服务接收到/compute请求时会进行大量的数值计算,致使没法响应其余的请求(/)。node
在Java语言中能够经过多线程的方式来解决上述的问题,可是Node.js在代码执行的时候是单线程的,那么Node.js应该如何解决上面的问题呢?其实Node.js能够建立一个子进程执行密集的cpu计算任务(例如上面例子中的longComputation)来解决问题,而child_process模块正是用来建立子进程的。shell
child_process提供了几种建立子进程的方式bootstrap
首先介绍一下spawn方法api
child_process.spawn(command[, args][, options]) command: 要执行的指令 args: 传递参数 options: 配置项
const { spawn } = require('child_process'); const child = spawn('pwd');
pwd是shell的命令,用于获取当前的目录,上面的代码执行完控制台并无任何的信息输出,这是为何呢?数组
控制台之因此不能看到输出信息的缘由是因为子进程有本身的stdio流(stdin、stdout、stderr),控制台的输出是与当前进程的stdio绑定的,所以若是但愿看到输出信息,能够经过在子进程的stdout 与当前进程的stdout之间创建管道实现浏览器
child.stdout.pipe(process.stdout);
也能够监听事件的方式(子进程的stdio流都是实现了EventEmitter API的,因此能够添加事件监听)缓存
child.stdout.on('data', function(data) { process.stdout.write(data); });
在Node.js代码里使用的console.log其实底层依赖的就是process.stdout多线程
除了创建管道以外,还能够经过子进程和当前进程共用stdio的方式来实现异步
const { spawn } = require('child_process'); const child = spawn('pwd', { stdio: 'inherit' });
stdio选项用于配置父进程和子进程之间创建的管道,因为stdio管道有三个(stdin, stdout, stderr)所以stdio的三个可能的值实际上是数组的一种简写
因为inherit方式使得子进程直接使用父进程的stdio,所以能够看到输出
ignore用于忽略子进程的输出(将/dev/null指定为子进程的文件描述符了),所以当ignore时child.stdout是null。
spawn默认状况下并不会建立子shell来执行命令,所以下面的代码会报错
const { spawn } = require('child_process'); const child = spawn('ls -l'); child.stdout.pipe(process.stdout); // 报错 events.js:167 throw er; // Unhandled 'error' event ^ Error: spawn ls -l ENOENT at Process.ChildProcess._handle.onexit (internal/child_process.js:229:19) at onErrorNT (internal/child_process.js:406:16) at process._tickCallback (internal/process/next_tick.js:63:19) at Function.Module.runMain (internal/modules/cjs/loader.js:746:11) at startup (internal/bootstrap/node.js:238:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3) Emitted 'error' event at: at Process.ChildProcess._handle.onexit (internal/child_process.js:235:12) at onErrorNT (internal/child_process.js:406:16) [... lines matching original stack trace ...] at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)
若是须要传递参数的话,应该采用数组的方式传入
const { spawn } = require('child_process'); const child = spawn('ls', ['-l']); child.stdout.pipe(process.stdout);
若是要执行ls -l | wc -l
命令的话能够采用建立两个spawn命令的方式
const { spawn } = require('child_process'); const child = spawn('ls', ['-l']); const child2 = spawn('wc', ['-l']); child.stdout.pipe(child2.stdin); child2.stdout.pipe(process.stdout);
也可使用exec
const { exec } = require('child_process'); exec('ls -l | wc -l', function(err, stdout, stderr) { console.log(stdout); });
因为exec会建立子shell,因此能够直接执行shell管道命令。spawn采用流的方式来输出命令的执行结果,而exec也是将命令的执行结果缓存起来统一放在回调函数的参数里面,所以exec只适用于命令执行结果数据小的状况。
其实spawn也能够经过配置shell option的方式来建立子shell进而支持管道命令,以下所示
const { spawn, execFile } = require('child_process'); const child = spawn('ls -l | wc -l', { shell: true }); child.stdout.pipe(process.stdout);
配置项除了stdio、shell以外还有cwd、env、detached等经常使用的选项
cwd用于修改命令的执行目录
const { spawn, execFile, fork } = require('child_process'); const child = spawn('ls -l | wc -l', { shell: true, cwd: '/usr' }); child.stdout.pipe(process.stdout);
env用于指定子进程的环境变量(若是不指定的话,默认获取当前进程的环境变量)
const { spawn, execFile, fork } = require('child_process'); const child = spawn('echo $NODE_ENV', { shell: true, cwd: '/usr' }); child.stdout.pipe(process.stdout); NODE_ENV=randal node b.js // 输出结果 randal
若是指定env的话就会覆盖掉默认的环境变量,以下
const { spawn, execFile, fork } = require('child_process'); spawn('echo $NODE_TEST $NODE_ENV', { shell: true, stdio: 'inherit', cwd: '/usr', env: { NODE_TEST: 'randal-env' } }); NODE_ENV=randal node b.js // 输出结果 randal
detached用于将子进程与父进程断开链接
例如假设存在一个长时间运行的子进程
// timer.js while(true) { }
可是主进程并不须要长时间运行的话就能够用detached来断开两者之间的链接
const { spawn, execFile, fork } = require('child_process'); const child = spawn('node', ['timer.js'], { detached: true, stdio: 'ignore' }); child.unref();
当调用子进程的unref方法时,同时配置子进程的stdio为ignore时,父进程就能够独立退出了
execFile与exec不一样,execFile一般用于执行文件,并且并不会建立子shell环境
fork方法是spawn方法的一个特例,fork用于执行js文件建立Node.js子进程。并且fork方式建立的子进程与父进程之间创建了IPC通讯管道,所以子进程和父进程之间能够经过send的方式发送消息。
注意:fork方式建立的子进程与父进程是彻底独立的,它拥有单独的内存,单独的V8实例,所以并不推荐建立不少的Node.js子进程
fork方式的父子进程之间的通讯参照下面的例子
parent.js
const { fork } = require('child_process'); const forked = fork('child.js'); forked.on('message', (msg) => { console.log('Message from child', msg); }); forked.send({ hello: 'world' });
child.js
process.on('message', (msg) => { console.log('Message from parent:', msg); }); let counter = 0; setInterval(() => { process.send({ counter: counter++ }); }, 1000);
node parent.js // 输出结果 Message from parent: { hello: 'world' } Message from child { counter: 0 } Message from child { counter: 1 } Message from child { counter: 2 } Message from child { counter: 3 } Message from child { counter: 4 } Message from child { counter: 5 } Message from child { counter: 6 }
回到本文初的那个问题,咱们就能够将密集计算的逻辑放到单独的js文件中,而后再经过fork的方式来计算,等计算完成时再通知主进程计算结果,这样避免主进程繁忙的状况了。
compute.js
const longComputation = () => { let sum = 0; for (let i = 0; i < 1e10; i++) { sum += i; }; return sum; }; process.on('message', (msg) => { const sum = longComputation(); process.send(sum); });
index.js
const http = require('http'); const { fork } = require('child_process'); const server = http.createServer(); server.on('request', (req, res) => { if (req.url === '/compute') { const compute = fork('compute.js'); compute.send('start'); compute.on('message', sum => { res.end(`Sum is ${sum}`); }); } else { res.end('Ok') } }); server.listen(3000);
经过前述几种方式建立的子进程都实现了EventEmitter,所以能够针对进程进行事件监听
经常使用的事件包括几种:close、exit、error、message
close事件当子进程的stdio流关闭的时候才会触发,并非子进程exit的时候close事件就必定会触发,由于多个子进程能够共用相同的stdio。
close与exit事件的回调函数有两个参数code和signal,code代码子进程最终的退出码,若是子进程是因为接收到signal信号终止的话,signal会记录子进程接受的signal值。
先看一个正常退出的例子
const { spawn, exec, execFile, fork } = require('child_process'); const child = exec('ls -l', { timeout: 300 }); child.on('exit', function(code, signal) { console.log(code); console.log(signal); }); // 输出结果 0 null
再看一个由于接收到signal而终止的例子,应用以前的timer文件,使用exec执行的时候并指定timeout
const { spawn, exec, execFile, fork } = require('child_process'); const child = exec('node timer.js', { timeout: 300 }); child.on('exit', function(code, signal) { console.log(code); console.log(signal); }); // 输出结果 null SIGTERM
注意:因为timeout超时的时候error事件并不会触发,而且当error事件触发时exit事件并不必定会被触发
error事件的触发条件有如下几种:
注意当代码执行出错的时候,error事件并不会触发,exit事件会触发,code为非0的异常退出码
const { spawn, exec, execFile, fork } = require('child_process'); const child = exec('ls -l /usrs'); child.on('error', function(code, signal) { console.log(code); console.log(signal); }); child.on('exit', function(code, signal) { console.log('exit'); console.log(code); console.log(signal); }); // 输出结果 exit 1 null
message事件适用于父子进程之间创建IPC通讯管道的时候的信息传递,传递的过程当中会经历序列化与反序列化的步骤,所以最终接收到的并不必定与发送的数据相一致。
sub.js
process.send({ foo: 'bar', baz: NaN });
const cp = require('child_process'); const n = cp.fork(`${__dirname}/sub.js`); n.on('message', (m) => { console.log('got message:', m); // got message: { foo: 'bar', baz: null } });
关于message有一种特殊状况要注意,下面的message并不会被子进程接收到
const { fork } = require('child_process'); const forked = fork('child.js'); forked.send({ cmd: "NODE_foo", hello: 'world' });
当发送的消息里面包含cmd属性,而且属性的值是以NODE_
开头的话,这样的消息是提供给Node.js自己保留使用的,所以并不会发出message
事件,而是会发出internalMessage
事件,开发者应该避免这种类型的消息,而且应当避免监听internalMessage
事件。
message除了发送字符串、object以外还支持发送server对象和socket对象,正由于支持socket对象才能够作到多个Node.js进程监听相同的端口号。
未完待续......