背景
由於 ons(阿里雲 RocketMQ 包)基於 C艹 封裝而來,不支援單一程式內例項化多個生產者與消費者,為了解決這一問題,使用了 Node.js 子程式。
在使用的過程中碰到的坑
- 釋出:程式管理關閉主程式後,子程式變為作業系統程式(pid 為 1)
幾種解決方案
- 將子程式看做獨立執行的程式,記錄 pid,釋出時程式管理關閉主程式同時關閉子程式
- 主程式監聽關閉事件,主動關閉從屬於自己的子程式
子程式種類
- spawn:執行命令
- exec:執行命令(新建 shell)
- execFile:執行檔案
- fork:執行檔案
子程式常用事件
- exit
- close
- error
- message
close 與 exit 是有區別的,close 是在資料流關閉時觸發的事件,exit 是在子程式退出時觸發的事件。因為多個子程式可以共享同一個資料流,所以當某個子程式 exit 時不一定會觸發 close 事件,因為這個時候還存在其他子程式在使用資料流。
子程式資料流
- stdin
- stdout
- stderr
因為是以主程式為出發點,所以子程式的資料流與常規理解的資料流方向相反,stdin:寫入流,stdout、stderr:讀取流。
spawn
spawn(command[, args][, options])
執行一條命令,通過 data 資料流返回各種執行結果。
基礎使用
const { spawn } = require(`child_process`);
const child = spawn(`find`, [ `.`, `-type`, `f` ]);
child.stdout.on(`data`, (data) => {
console.log(`child stdout:
${data}`);
});
child.stderr.on(`data`, (data) => {
console.error(`child stderr:
${data}`);
});
child.on(`exit`, (code, signal) => {
console.log(`child process exit with: code ${code}, signal: ${signal}`);
});
複製程式碼
常用引數
{
cwd: String,
env: Object,
stdio: Array | String,
detached: Boolean,
shell: Boolean,
uid: Number,
gid: Number
}
複製程式碼
重點說明下 detached 屬性,detached 設定為 true
是為子程式獨立執行做準備。子程式的具體行為與作業系統相關,不同系統表現不同,Windows 系統子程式會擁有自己的控制檯視窗,POSIX 系統子程式會成為新程式組與會話負責人。
這個時候子程式還沒有完全獨立,子程式的執行結果會展示在主程式設定的資料流上,並且主程式退出會影響子程式執行。當 stdio 設定為 ignore
並呼叫 child.unref();
子程式開始真正獨立執行,主程式可獨立退出。
exec
exec(command[, options][, callback])
執行一條命令,通過回撥引數返回結果,指令未執行完時會快取部分結果到系統記憶體。
const { exec } = require(`child_process`);
exec(`find . -type f | wc -l`, (err, stdout, stderr) => {
if (err) {
console.error(`exec error: ${err}`);
return;
}
console.log(`Number of files ${stdout}`);
});
複製程式碼
兩全其美 —— spawn 代替 exec
由於 exec 的結果是一次性返回,在返回前是快取在記憶體中的,所以在執行的 shell 命令輸出過大時,使用 exec 執行命令的方式就無法按期望完成我們的工作,這個時候可以使用 spawn 代替 exec 執行 shell 命令。
const { spawn } = require(`child_process`);
const child = spawn(`find . -type f | wc -l`, {
stdio: `inherit`,
shell: true
});
child.stdout.on(`data`, (data) => {
console.log(`child stdout:
${data}`);
});
child.stderr.on(`data`, (data) => {
console.error(`child stderr:
${data}`);
});
child.on(`exit`, (code, signal) => {
console.log(`child process exit with: code ${code}, signal: ${signal}`);
});
複製程式碼
execFile
child_process.execFile(file[, args][, options][, callback])
執行一個檔案
與 exec 功能基本相同,不同之處在於執行給定路徑的一個指令碼檔案,並且是直接建立一個新的程式,而不是建立一個 shell 環境再去執行指令碼,相對更輕量級更高效。但是在 Windows 系統中如 .cmd
、.bat
等檔案無法直接執行,這是 execFile 就無法工作,可以使用 spawn、exec 代替。
fork
child_process.fork(modulePath[, args][, options])
執行一個 Node.js 檔案
// parent.js
const { fork } = require(`child_process`);
const child = fork(`child.js`);
child.on(`message`, (msg) => {
console.log(`Message from child`, msg);
});
child.send({ hello: `world` });
複製程式碼
// child.js
process.on(`message`, (msg) => {
console.log(`Message from parent:`, msg);
});
let counter = 0;
setInterval(() => {
process.send({ counter: counter++ });
}, 3000);
複製程式碼
fork 實際是 spawn 的一種特殊形式,固定 spawn Node.js 程式,並且在主子程式間建立了通訊通道,讓主子程式可以使用 process 模組基於事件進行通訊。
子程式使用場景
- 計算密集型系統
- 前端構建工具利用多核 CPU 平行計算,提升構建效率
- 程式管理工具,如:PM2 中部分功能
實踐:Akyuu PM 啟動專案
- 程式 ①:用來解析使用者輸入,呼叫啟動命令
const commander = require(`commander`); const path = require(`path`); const start = require(`./lib/start`); commander .command(`start <entry>`) .description(`start process`) .option(`--dev`, `set dev environment`) .action(function(entry, options) { start({ entry: path.resolve(__dirname, `../entry/${entry}`), isDaemon: !options.dev }); }); 複製程式碼
- 程式 ①:
- 利用 spawn 啟動指定入口檔案程式 ②, 設定 detached 為
true
,呼叫child.unref();
使子程式獨立執行const { spawn } = require(`child_process`); const child = spawn(process.execPath, [ path.resolve(__dirname, `cluster`) ], { cwd: path.resolve(__dirname, `../../`), env: Object.assign({}, process.env, { izayoiCoffee: JSON.stringify({ configDir: config.akyuuConfigDir, entry: options.entry }) }), detached: true, stdio: `ignore` }); child.on(`exit`, function(code, signal) { console.error(`start process `${path.basename(options.entry)}` failed, ` + `code: ${code}, signal: ${signal}`); process.exit(1); }); child.unref(); 複製程式碼
- 記錄子程式 pid 到檔案
child .on(`fork`, function(worker) { try { fs.writeFileSync( `pid file path`, worker.process.pid, { encoding: `utf8` } ); } catch(err) { console.error( `[%s] [uncaughtException] [master: %d] %s`, moment().utcOffset(8).format(`YYYY-MM-DD HH:mm:ss.SSS`), process.pid, err.stack ); } }) .on(`exit`, function(worker, code, signal) { try { fs.unlinkSync(`pid file path`); } catch(err) { console.error( `[%s] [uncaughtException] [master: %d] %s`, moment().utcOffset(8).format(`YYYY-MM-DD HH:mm:ss.SSS`), process.pid, err.stack ); } }); 複製程式碼
- 利用 spawn 啟動指定入口檔案程式 ②, 設定 detached 為
- 程式 ②:如果程式 ② 中還需要啟動自己的子程式,在啟動子程式後,監聽自己的退出事件,並主動關閉子程式,防止子程式變為作業系統程式而不受控
const { fork } = require(`child_process); const child = fork(`some child process file`); // 程式停止訊號 process.on(`SIGHUP`, function() { child.kill(`SIGHUP`); process.exit(0); }); // kill 預設引數訊號 process.on(`SIGTERM`, function() { child.kill(`SIGHUP`); process.exit(0); }); // Ctrl + c 訊號 process.on(`SIGINT`, function() { child.kill(`SIGHUP`); process.exit(0); }); // 退出事件 process.on(`exit`, function() { child.kill(`SIGHUP`); process.exit(0); }); // 未捕獲異常 process.on(`uncaughtException`, function() { child.kill(`SIGHUP`); process.exit(0); }); 複製程式碼
總結
在使用 Node.js 做開發中,尤其是 API 開發過程中很少涉及到子程式,但是子程式還是比較重要的一個組成部分。Node.js 可以利用子程式做些計算密集型任務,雖然沒有 C艹 等其他語言高效、方便,但是也不失為一種方案,在沒有掌握其他語言時可以用 Node.js 支撐起業務場景。對於子程式的採坑與使用在本文中記錄,以供未來的自己參考。