nodejs以單線程模式運行奴曙,但使用事件驅(qū)動處理并發(fā)由驹,有助于創(chuàng)建多個子進(jìn)程提高性能募疮。
默認(rèn)nodejs父子進(jìn)程會建立stdin炫惩、stdout、stderr的管道阿浓,以非阻塞方式在管道中流通他嚷。
child_process
-
child_process.exec(command[. options][, callback])
使用子進(jìn)程執(zhí)行命令,緩存子進(jìn)程的輸出芭毙,將子進(jìn)程的輸出以回調(diào)函數(shù)參數(shù)的形式返回筋蓖。
var exec = require('child_process').exec;
// 成功的例子
exec('ls -al', function(error, stdout, stderr){
if(error) {
console.error('error: ' + error);
return;
}
console.log('stdout: ' + stdout);
console.log('stderr: ' + typeof stderr);
});
// 失敗的例子
exec('ls hello.txt', function(error, stdout, stderr){
if(error) {
console.error('error: ' + error);
return;
}
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
});
-
child_process.spawn(command[, args][, options])
使用指定的命令行參數(shù)創(chuàng)建新進(jìn)程。 -
child_process.fork(modulePath[, args][, options])
是spawn()的特殊形式稿蹲,用于在子進(jìn)程運行的模塊扭勉,fork('/a.js')
相當(dāng)于`spawn('node, ['/a.js'])。fork會在父子進(jìn)程間建立通信管道苛聘,用于進(jìn)程間通信涂炎。
每個函數(shù)都返回ChildProcess實例忠聚,實例實現(xiàn)了Nodejs EventEmitter API,允許父進(jìn)程注冊監(jiān)聽器函數(shù)唱捣,在子進(jìn)程生命周期期間两蟀,特定的事件發(fā)生時調(diào)用這些函數(shù)。
exec()
, execFile()
, fork()
都是通過spawn()
實現(xiàn)的
spawn定義輸入輸出
const { spawn } = require("child-process");
const path = require("path");
let child = spawn("node", ["sub_process.js", "--port", "3000"], {
cwd: path.join(__dirname, ""test") // 指定子進(jìn)程的當(dāng)前工作目錄
stdio: [0, 1, 2] // 標(biāo)準(zhǔn)輸入震缭、標(biāo)準(zhǔn)輸出赂毯、錯誤輸出
});
// sub_process.js
process.stdout.write(process.argv.toString());
只有輸出,沒有通信拣宰,如果要通信党涕,stdio配置pipe(默認(rèn)),設(shè)置成ignore則輸出禁止
// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");
// 創(chuàng)建子進(jìn)程
let child = spawn("node", ["sub_process.js"], {
cwd: path.join(__dirname, "test"),
stdio: ["pipe"]
});
child.stdout.on("data", data => console.log(data.toString()));
// hello world
// 子進(jìn)程執(zhí)行 sub_process.js
process.stdout.write("hello world");
多進(jìn)程
// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");
// 創(chuàng)建子進(jìn)程
let child1 = spawn("node", ["sub_process_1.js", "--port", "3000"], {
cwd: path.join(__dirname, "test"),
});
let child2 = spawn("node", ["sub_process_2.js"], {
cwd: path.join(__dirname, "test"),
});
// 讀取子進(jìn)程 1 寫入的內(nèi)容巡社,寫入子進(jìn)程 2
child1.stdout.on("data", data => child2.stdout.write(data.toString));
// 文件:~test/sub_process_1.js
// 獲取 --port 和 3000
process.argv.slice(2).forEach(item => process.stdout.write(item));
// 文件:~test/sub_process_2.js
const fs = require("fs");
// 讀取主進(jìn)程傳遞的參數(shù)并寫入文件
process.stdout.on("data", data => {
fs.writeFile("param.txt", data, () => {
process.exit();
});
});
標(biāo)準(zhǔn)進(jìn)程通信
// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");
// 創(chuàng)建子進(jìn)程
let child = spawn("node", ["sub_process.js"], {
cwd: path.join(__dirname, "test"),
stdio: [0, "pipe", "ignore", "ipc"]
});
child.on("message", data => {
console.log(data);
// 回復(fù)消息給子進(jìn)程
child.send("world");
// 殺死子進(jìn)程
// process.kill(child.pid);
});
// hello
// 文件:~test/sub_process.js
// 給主進(jìn)程發(fā)送消息
process.send("hello");
// 接收主進(jìn)程回復(fù)的消息
process.on("message", data => {
console.log(data);
// 退出子進(jìn)程
process.exit();
});
// world
這種方式被稱為標(biāo)準(zhǔn)進(jìn)程通信膛堤,通過給 options 的 stdio 數(shù)組配置 ipc,只要數(shù)組中存在 ipc 即可晌该,一般放在數(shù)組開頭或結(jié)尾肥荔,配置 ipc 后子進(jìn)程通過調(diào)用自己的 send 方法發(fā)送消息給主進(jìn)程,主進(jìn)程中用子進(jìn)程的 message 事件進(jìn)行接收朝群,也可以在主進(jìn)程中接收消息的 message 事件的回調(diào)當(dāng)中燕耿,通過子進(jìn)程的 send 回復(fù)消息,并在子進(jìn)程中用 message 事件進(jìn)行接收姜胖,這樣的編程方式比較統(tǒng)一誉帅,更貼近于開發(fā)者的意愿。
退出谭期、殺死子進(jìn)程
// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");
// 創(chuàng)建子進(jìn)程
let child = spawn("node", ["sub_process.js"], {
cwd: path.join(__dirname, "test"),
stdio: [0, "pipe", "ignore", "ipc"]
});
child.on("message", data => {
console.log(data);
// 殺死子進(jìn)程
process.kill(child.pid);
});
// hello world
殺死子進(jìn)程的方法為 process.kill堵第,由于一個主進(jìn)程可能有多個子進(jìn)程,所以指定要殺死的子進(jìn)程需要傳入子進(jìn)程的 pid 屬性作為 process.kill 的參數(shù)隧出。
注意:退出子進(jìn)程 process.exit 方法是在子進(jìn)程中操作的,此時 process 代表子進(jìn)程阀捅,殺死子進(jìn)程 process.kill 是在主進(jìn)程中操作的胀瞪,此時 process 代表主進(jìn)程。
獨立子進(jìn)程
讓子進(jìn)程不受主進(jìn)程控制
// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");
// 創(chuàng)建子進(jìn)程
let child = spawn("node", ["sub_process.js"], {
cwd: path.join(__dirname, "test"),
stdio: "ignore",
detached: true
});
// 與主進(jìn)程斷絕關(guān)系
child.unref();
/ 文件:~test/sub_process.js
const fs = require("fs");
setInterval(() => {
fs.appendFileSync("test.txt", "hello");
});
要想創(chuàng)建的子進(jìn)程獨立饲鄙,需要在創(chuàng)建子進(jìn)程時配置 detached 參數(shù)為 true凄诞,表示該子進(jìn)程不受控制,還需調(diào)用子進(jìn)程的 unref 方法與主進(jìn)程斷絕關(guān)系忍级,但是僅僅這樣子進(jìn)程可能還是會受主進(jìn)程的影響帆谍,要想子進(jìn)程完全獨立需要保證子進(jìn)程一定不能和主進(jìn)程共用標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和錯誤輸出轴咱,也就是 stdio 必須設(shè)置為 ignore汛蝙,這也就代表著獨立的子進(jìn)程是不能和主進(jìn)程進(jìn)行標(biāo)準(zhǔn)進(jìn)程通信烈涮,即不能設(shè)置 ipc。
fork
fork是對spawn的封裝
// 文件:process.js
const fork = require("child_process");
const path = require("path");
// 創(chuàng)建子進(jìn)程
let child = fork("sub_process.js", ["--port", "3000"], {
cwd: path.join(__dirname, "test"),
silent: true
});
child.send("hello world");
// 文件:~test/sub_process.js
// 接收主進(jìn)程發(fā)來的消息
process.on("message", data => console.log(data));
fork 的用法與 spawn 相比有所改變窖剑,第一個參數(shù)是子進(jìn)程執(zhí)行文件的名稱坚洽,第二個參數(shù)為數(shù)組,存儲執(zhí)行時的參數(shù)和值西土,第三個參數(shù)為 options讶舰,其中使用 slilent 屬性替代了 spawn 的 stdio,當(dāng) silent 為 true 時需了,此時主進(jìn)程與子進(jìn)程的所有非標(biāo)準(zhǔn)通信的操作都不會生效跳昼,包括標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和錯誤輸出肋乍,當(dāng)設(shè)為 false 時可正常輸出鹅颊,返回值依然為一個子進(jìn)程。
fork 創(chuàng)建的子進(jìn)程可以直接通過 send 方法和監(jiān)聽 message 事件與主進(jìn)程進(jìn)行通信住拭。
fork的原理
// 文件:fork.js
const childProcess = require("child_process");
const path = require("path");
// 封裝原理
childProcess.fork = function (modulePath, args, options) {
let stdio = options.silent ? ["ignore", "ignore", "ignore", "ipc"] : [0, 1, 2, "ipc"];
return childProcess.spawn("node", [modulePath, ...args], {
...options,
stdio
});
}
// 創(chuàng)建子進(jìn)程
let child = fork("sub_process.js", ["--port", "3000"], {
cwd: path.join(__dirname, "test"),
silent: false
});
// 向子進(jìn)程發(fā)送消息
child.send("hello world");
// 文件:~test/sub_process.js
// 接收主進(jìn)程發(fā)來的消息
process.on("message", data => console.log(data));
// hello world
spawn 中的有一些 fork 沒有傳的參數(shù)(如使用 node 執(zhí)行文件)挪略,都在內(nèi)部調(diào)用 spawn 時傳遞默認(rèn)值或?qū)⒛J(rèn)參數(shù)與 fork 傳入的參數(shù)進(jìn)行整合,著重處理了 spawn 沒有的參數(shù) silent滔岳,其實就是處理成了 spawn 的 stdio 參數(shù)兩種極端的情況(默認(rèn)使用 ipc 通信)杠娱,封裝 fork 就是讓我們能更方便的創(chuàng)建子進(jìn)程,可以更少的傳參谱煤。
exec和execFile實現(xiàn)多進(jìn)程
execFile 和 exec 是 child_process 模塊的兩個方法摊求,execFile 是基于 spawn 封裝的,而 exec 是基于 execFile 封裝的刘离,這兩個方法用法大同小異室叉,execFile 可以直接創(chuàng)建子進(jìn)程進(jìn)行文件操作,而 exec 可以直接開啟子進(jìn)程執(zhí)行命令硫惕,常見的應(yīng)用場景如 http-server 以及 weboack-dev-server 等命令行工具在啟動本地服務(wù)時自動打開瀏覽器茧痕。
// execFile 和 exec
const { execFile, exec } = require("child_process");
let execFileChild = execFile("node", ["--version"], (err, stdout, stderr) => {
if (error) throw error;
console.log(stdout);
console.log(stderr);
});
let execChild = exec("node --version", (err, stdout, stderr) => {
if (err) throw err;
console.log(stdout);
console.log(stderr);
});
cluster
開啟進(jìn)程需要消耗內(nèi)存,所以開啟進(jìn)程的數(shù)量要適合恼除,合理運用多進(jìn)程可以大大提高效率踪旷,如 Webpack 對資源進(jìn)行打包,就開啟了多個進(jìn)程同時進(jìn)行豁辉,大大提高了打包速度令野,集群也是多進(jìn)程重要的應(yīng)用之一,用多個進(jìn)程同時監(jiān)聽同一個服務(wù)徽级,一般開啟進(jìn)程的數(shù)量跟 CPU 核數(shù)相同為好气破,此時多個進(jìn)程監(jiān)聽的服務(wù)會根據(jù)請求壓力分流處理,也可以通過設(shè)置每個子進(jìn)程處理請求的數(shù)量來實現(xiàn) “負(fù)載均衡”餐抢。
使用ipc實現(xiàn)集群
ipc 標(biāo)準(zhǔn)進(jìn)程通信使用 send 方法發(fā)送消息時第二個參數(shù)支持傳入一個服務(wù)现使,必須是 http 服務(wù)或者 tcp 服務(wù)低匙,子進(jìn)程通過 message 事件進(jìn)行接收,回調(diào)的參數(shù)分別對應(yīng)發(fā)送的參數(shù)朴下,即第一個參數(shù)為消息努咐,第二個參數(shù)為服務(wù),我們就可以在子進(jìn)程創(chuàng)建服務(wù)并對主進(jìn)程的服務(wù)進(jìn)行監(jiān)聽和操作(listen 除了可以監(jiān)聽端口號也可以監(jiān)聽服務(wù))殴胧,便實現(xiàn)了集群渗稍,代碼如下。
// 文件:server.js
const os = require("os"); // os 模塊用于獲取系統(tǒng)信息
const http = require("http");
const path = require("path");
const { fork } = require("child_process");
// 創(chuàng)建服務(wù)
const server = http.createServer((res, req) => {
res.end("hello");
}).listen(3000);
// 根據(jù) CPU 個數(shù)創(chuàng)建子進(jìn)程
os.cpus().forEach(() => {
fork("child_server.js", {
cwd: path.join(__dirname);
}).send("server", server);
});
// 文件:child_server.js
const http = require("http");
// 接收來自主進(jìn)程發(fā)來的服務(wù)
process.on("message", (data, server) => {
process.stdout.write(`child${process.pid}`);
http.createServer((req, res) => {
res.end(`child${process.pid}`);
}).listen(server); // 子進(jìn)程共用主進(jìn)程的服務(wù)
});
使用cluster實現(xiàn)集群
cluster 模塊是 NodeJS 提供的用來實現(xiàn)集群的团滥,他將 child_process 創(chuàng)建子進(jìn)程的方法集成進(jìn)去竿屹,實現(xiàn)方式要比使用 ipc 更簡潔。
// 文件:cluster.js
const cluster = require("cluster");
const http = require("http");
const os = require("os");
// 判斷當(dāng)前執(zhí)行的進(jìn)程是否為主進(jìn)程灸姊,為主進(jìn)程則創(chuàng)建子進(jìn)程拱燃,否則用子進(jìn)程監(jiān)聽服務(wù)
if (cluster.isMaster) {
// 創(chuàng)建子進(jìn)程
os.cpus().forEach(() => cluster.fork());
} else {
// 創(chuàng)建并監(jiān)聽服務(wù)
http.createServer((req, res) => {
res.end(`child${process.pid}`);
}).listen(3000);
}
上面代碼既會執(zhí)行 if 又會執(zhí)行 else,這看似很奇怪力惯,但其實不是在同一次執(zhí)行的碗誉,主進(jìn)程執(zhí)行時會通過 cluster.fork 創(chuàng)建子進(jìn)程,當(dāng)子進(jìn)程被創(chuàng)建會將該文件再次執(zhí)行父晶,此時則會執(zhí)行 else 中對服務(wù)的監(jiān)聽哮缺,還有另一種用法將主進(jìn)程和子進(jìn)程執(zhí)行的代碼拆分開,邏輯更清晰甲喝,用法如下尝苇。
// 文件:cluster.js
const cluster = require("cluster");
const path = require("path");
const os = require("os");
// 設(shè)置子進(jìn)程讀取文件的路徑
cluster.setupMaster({
exec: path.join(__dirname, "cluster-server.js")
});
// 創(chuàng)建子進(jìn)程
os.cpus().forEach(() => cluster.fork());
/// 文件:cluster-server.js
const http = require("http");
// 創(chuàng)建并監(jiān)聽服務(wù)
http.createServer((req, res) => {
res.end(`child${process.pid}`);
}).listen(3000);
通過 cluster.setupMaster 設(shè)置子進(jìn)程執(zhí)行文件以后,就可以將主進(jìn)程和子進(jìn)程的邏輯拆分開埠胖,在實際的開發(fā)中這樣的方式也是最常用的糠溜,耦合度低,可讀性好直撤,更符合開發(fā)的原則非竿。