在介紹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進程監聽相同的端口號。
未完待續......