Nodejs child_process

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ā)的原則非竿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谋竖,隨后出現(xiàn)的幾起案子汽馋,更是在濱河造成了極大的恐慌,老刑警劉巖圈盔,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異悄雅,居然都是意外死亡驱敲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門宽闲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來众眨,“玉大人握牧,你說我怎么就攤上這事∶淅妫” “怎么了沿腰?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長狈定。 經(jīng)常有香客問我颂龙,道長,這世上最難降的妖魔是什么纽什? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任措嵌,我火速辦了婚禮,結(jié)果婚禮上芦缰,老公的妹妹穿的比我還像新娘企巢。我一直安慰自己,他們只是感情好让蕾,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布浪规。 她就那樣靜靜地躺著,像睡著了一般探孝。 火紅的嫁衣襯著肌膚如雪笋婿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天再姑,我揣著相機(jī)與錄音萌抵,去河邊找鬼。 笑死元镀,一個胖子當(dāng)著我的面吹牛绍填,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播栖疑,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼讨永,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遇革?” 一聲冷哼從身側(cè)響起卿闹,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎萝快,沒想到半個月后锻霎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡揪漩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年旋恼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奄容。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡冰更,死狀恐怖产徊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜀细,我是刑警寧澤舟铜,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站奠衔,受9級特大地震影響谆刨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涣觉,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一痴荐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧官册,春花似錦生兆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至员淫,卻和暖如春合蔽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背介返。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工拴事, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人圣蝎。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓刃宵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親徘公。 傳聞我的和親對象是個殘疾皇子牲证,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容