Node.js 的子程序模組 (child_process)
Node.js 是單執行緒 (single-threaded) 的模型架構,可提升 CPU 使用率,但無論你的伺服器多麼強大,一個執行緒的負載量總是有限的。而子程序 (child_process,又稱子進程) 模組的存在就是 Node.js 可以實現多核 CPU 運用的方法。
為什麼要用子程序?
我們一般的作業系統 (operating system) 有不同的程序 (process) 在背景執行,每個程序都被 CPU 的一個核心所管理,在被呼叫時執行不同的運算。因此如果所有的程式只用一個核心去跑就太可惜了,我們需要有效的運用多核心去跑各式程序,同時也要把不同的運算邏輯分配給不同的程序。
如下圖,因為 Node.js 是利用事件驅動 (event-driven) 的方式來處理併發的需求,因此我們可以在系統上創建多個子程序,透過系統命令設定父子程序來優化 CPU 計算的問題、或進行多程序開發等,極大化 Node.js 的開發能力。
透過子程序,我們可以寫入、讀取資料流,並使用 Linux 命令像操作作業系統一樣的操作子程序,再把執行結果拿去主程序使用。
子程序的執行結果會先儲存在系統緩存中,等到結束執行後,主程序可再用回調函數讀取子程序運行的結果。
種類
spawn:執行命令(不會新建 shell)
文檔上說這是使用子程序最主要的一個方法。
child_process.spawn()
異步衍生子程序,不阻塞 Node.js 事件循環。
child_process.spawnSync()
同步的方式提供了等效的功能,會阻塞事件循環直到衍生的子程序退出或終止。
spawn 方法創建一個子進程來執行特定命令,用法與 execFile 方法類似,但是沒有回調函數,只能通過監聽事件,來獲取運行結果。它屬於異步執行,適用於子進程長時間運行的情況。
exec:執行命令(會新建 shell)
child_process.exec()
建立一個 shell 並在該 shell 中執行命令,完成時回傳 stdout
和 stderr
於回調函數。
execFile:執行檔案
child_process.execFile()
類似 child_process.exec()
,但是預設為直接衍生命令而不先建立 shell。
child_process.execFileSync()
為child_process.execFile()
的同步版本,會阻塞 Node.js 事件循環。
fork:執行檔案
child_process.fork()
建立一个新的 Node.js 程序,並調用一個指定的模塊,建立父子進程中間的通信通道,允許相互發送消息。
子程序資料流 (pipe)
子程序建立後,中間會有資料流跟主程序串通。
- stdin 寫入流
- stdout 讀取流:執行結果
- stderr 讀取流:執行錯誤
常用事件
可利用 Node.js 的 EventEmitter
API 設定事件監聽器,在子程序的生命周期中當發生某些事件時調用。根據文檔,常用事件如下:
exit
當子進程結束後時會觸發
'exit'
事件。 如果進程退出,則 code 是進程的最終退出碼,否則為 null。 如果進程是因為收到的信號而終止,則 signal 是信號的字元串名稱,否則為 null。 這兩個值至少有一個是非空的。當
'exit'
事件被觸發時,子進程的 stdio 流可能依然是打開的。
close
當子進程的 stdio 流已被關閉時會觸發
'close'
事件。 這與'exit'
事件不同,因為多個進程可能共用相同的 stdio 流。當 ‘exit’ 事件被觸發時,子進程的 stdio 流可能依然是打開的。
disconnect
調用父進程中的 subprocess.disconnect() 或子進程中的 process.disconnect() 後會觸發
'disconnect'
事件。 斷開連接後就不能再發送或接收信息,且 subprocess.connected 屬性為 false。
error
每當出現以下情況時觸發
'error'
事件:無法衍生進程;
無法殺死進程;
向子進程發送消息失敗。
發生錯誤後,可能會也可能不會觸發 ‘exit’ 事件。 當同時監聽 ‘exit’ 和 ‘error’ 事件時,則需要防止意外地多次調用處理函數。
message
使用
process.send()
發送消息時會觸發'message'
事件。
使用場景
計算密集型系統
前端構建工具利用多核 CPU 平行計算,提升構建效率
程序管理工具,如:PM2 中部分功能
舉例
用 spawn 查看資料夾內容,因為沒有回調函數所以設定事件監聽:
const { spawn } = require('child_process');
const child = spawn('ls', ['-lh']);child.on('exit', (code) => {
console.log(`Child process exited with code ${code}`);
});child.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});child.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
用 execFile 執行指定的檔案,並用回調函數讀取執行結果、做錯誤處理:
const { execFile } = require('child_process');const child = execFile('node', ['test.js'], (error, stdout, stderr) => {
if (error) {
console.error('stderr', stderr);
throw error;
}
console.log('stdout', stdout);
});
Timeout 處理
為了避免子程序跑一些迴圈跑到掛掉,exec 可以用 timeout 參數設定超時處理。
但 spawn 沒有這個參數(因為本來就是定位跑比較長的子程序用),一個 workaround 是加上以下這段程式碼,殺死子程序(我寫在 promise 裡面所以用 reject,ls 是執行 spawn 的子程序):
// timeout after 10 sec
setTimeout(() => {
ls.kill();
reject('EXECUTION TIMED OUT');
}, 10000);
Out of Memory 處理
為了避免子程序跑到記憶體爆炸,可以用 — max-old-space-size 設定最大使用記憶體上限的參數做處理。
- Note: 一旦子程序執行時使用超過設定的記憶體上限,子程序會終止並自動生成一份錯誤報告在專案中,如果要調整也可以在參數上設定報告的形式。
const child = spawn("node", [`--max-old-space-size=10`, `test.js`]);