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请求时会进行大量的数值计算,导致无法响应其他的请求(/)。
在Java语言中可以通过多线程的方式来解决上述的问题,但是Node.js在代码执行的时候是单线程的,那么Node.js应该如何解决上面的问题呢?其实Node.js可以创建一个子进程执行密集的cpu计算任务(例如上面例子中的longComputation)来解决问题,而child_process模块正是用来创建子进程的。
创建子进程的方式
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
首先介绍一下spawn方法
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的三个可能的值其实是数组的一种简写
pipe 相当于['pipe', 'pipe', 'pipe'](默认值)
ignore 相当于['ignore', 'ignore', 'ignore']
inherit 相当于[process.stdin, process.stdout, process.stderr]
由于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用于指定子进程的环境变量(如果不指定的话,默认获取当前进程的环境变量)