Node.js 的子程序模組 (child_process)

Wenchin
Wenchin Rolls Around
8 min readFeb 2, 2020
Photo by Hello I’m Nik on Unsplash

Node.js 是單執行緒 (single-threaded) 的模型架構,可提升 CPU 使用率,但無論你的伺服器多麼強大,一個執行緒的負載量總是有限的。而子程序 (child_process,又稱子進程) 模組的存在就是 Node.js 可以實現多核 CPU 運用的方法。

為什麼要用子程序?

我們一般的作業系統 (operating system) 有不同的程序 (process) 在背景執行,每個程序都被 CPU 的一個核心所管理,在被呼叫時執行不同的運算。因此如果所有的程式只用一個核心去跑就太可惜了,我們需要有效的運用多核心去跑各式程序,同時也要把不同的運算邏輯分配給不同的程序。

如下圖,因為 Node.js 是利用事件驅動 (event-driven) 的方式來處理併發的需求,因此我們可以在系統上創建多個子程序,透過系統命令設定父子程序來優化 CPU 計算的問題、或進行多程序開發等,極大化 Node.js 的開發能力。

from c-sharpcorner

透過子程序,我們可以寫入、讀取資料流,並使用 Linux 命令像操作作業系統一樣的操作子程序,再把執行結果拿去主程序使用。

子程序的執行結果會先儲存在系統緩存中,等到結束執行後,主程序可再用回調函數讀取子程序運行的結果。

種類

spawn:執行命令(不會新建 shell)

文檔上說這是使用子程序最主要的一個方法。

child_process.spawn()異步衍生子程序,不阻塞 Node.js 事件循環。

child_process.spawnSync() 同步的方式提供了等效的功能,會阻塞事件循環直到衍生的子程序退出或終止。

spawn 方法創建一個子進程來執行特定命令,用法與 execFile 方法類似,但是沒有回調函數,只能通過監聽事件,來獲取運行結果。它屬於異步執行,適用於子進程長時間運行的情況。

exec:執行命令(會新建 shell)

child_process.exec()建立一個 shell 並在該 shell 中執行命令,完成時回傳 stdoutstderr 於回調函數。

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`]);

Reference

--

--