文章翻譯自Node.js Child Processes: Everything you need to know
如何使用spawn函數(shù)、exec函數(shù)钥星、execFile函數(shù)和for函數(shù)
Node.js中的非阻塞單線程的特性對單進(jìn)程任務(wù)是非常有用沾瓦。但是事實上,面對日益復(fù)雜的業(yè)務(wù)邏輯谦炒,單個cpu中的單進(jìn)程所能提供的計算力顯然是不足的贯莺。因為無論服務(wù)器如何強大,單線程只可以利用有限的資源宁改。
事實上缕探,Node.js運行在單線程上,并不意味著開發(fā)者不能利用多進(jìn)程透且,當(dāng)然還有多臺服務(wù)器撕蔼。
使用多進(jìn)程是擴展Node.js程序最佳的方式。Node.js就是為在多個節(jié)點秽誊,創(chuàng)建分布式應(yīng)用而設(shè)計的鲸沮。這也是取名Node的原因」郏可伸縮性已經(jīng)滲透到平臺中讼溺,因此開發(fā)不能等到應(yīng)用程序運行到生命周期后期,在開始思考這個問題最易。
請注意怒坯,在閱讀本篇文章前你應(yīng)該理解Node.js事件和Node.js流的相關(guān)知識。如果你還沒準(zhǔn)備好藻懒,我推薦你閱讀下面兩篇文章:
Node.js事件驅(qū)動
你應(yīng)該知道的Node.js流
子進(jìn)程模塊
開發(fā)者通過Node的child_process模塊剔猿,可以很容易衍生出子進(jìn)程。這些子系統(tǒng)可以通過消息系統(tǒng)實現(xiàn)相互通信嬉荆。
開發(fā)者可以通過child_process模塊的內(nèi)部命令归敬,來訪問操作系統(tǒng)。
開發(fā)者可以控制子進(jìn)程的輸入流,監(jiān)聽其輸出流汪茧。開發(fā)者同樣可以控制輸入底層操作系統(tǒng)命令的參數(shù)椅亚、并且對命令的輸出做任何所需要的改動。由于命令的輸入與輸出數(shù)據(jù)都可以被Node.js流處理舱污,因此開發(fā)者可以將一個命令的輸出(就像linux命令那樣)作為另一個命令源數(shù)據(jù)呀舔。
注意本文中所有的例子都是基于linux系統(tǒng),如果你使用的系統(tǒng)時windows系統(tǒng)扩灯,你需要將對應(yīng)的linux命令換成windows命令媚赖。
在Node.js中有四種函數(shù)創(chuàng)建子進(jìn)程:spawn()、fork()驴剔、exec()和execFile()省古。
接下來粥庄,我們將會討論這四種函數(shù)間的不同函數(shù)的應(yīng)用場景丧失。
Spawn(衍生)子進(jìn)程
Spawan函數(shù)可以衍生出新的子進(jìn)程,并通過Spwan函數(shù)向子進(jìn)程傳遞命令惜互。例如布讹,通過衍生的子進(jìn)程,執(zhí)行"pwd"命令:
const { spawn } = require('child_process');
const child = spawn('pwd');
Node.js程序從child_process模塊析構(gòu)出spawn函數(shù)训堆,向函數(shù)傳遞OS命令描验,并在子進(jìn)程中執(zhí)行OS命令。
執(zhí)行spawn函數(shù)的結(jié)果是繼承事件接口的子進(jìn)程實例對象坑鱼,開發(fā)者可以對它直接注冊事件處理函數(shù)膘流。例如開發(fā)者對子進(jìn)程執(zhí)行結(jié)果和子進(jìn)程退出行為注冊事件:
child.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
child.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
child.on('exit', function (code, signal) {
console.log('child process exited with ' +
`code ${code} and signal ${signal}`);
});
開發(fā)者對子進(jìn)程還可以注冊的處理事件有:disconnect、error和message鲁沥。
- disconnect事件:當(dāng)父進(jìn)程調(diào)用child.disconnect函數(shù)時觸發(fā)
- error事件:當(dāng)進(jìn)程不能衍生或者進(jìn)程被殺死時觸發(fā)
- close事件:當(dāng)子進(jìn)程的stdio流關(guān)閉時觸發(fā)
- message事件:當(dāng)子進(jìn)程使用process.send()函數(shù)時觸發(fā)呼股,這個函數(shù)主要用于父子進(jìn)程間的通信。
每個子進(jìn)程都具有標(biāo)準(zhǔn)的stdio流画恰,開發(fā)者可以通過child.stdin彭谁、child.stdout和child.stderr操作stdio流。
在子進(jìn)程中的stdio流關(guān)閉時允扇,子進(jìn)程會觸發(fā)close事件缠局。close事件并不完全等同于exist事件,主要在于子進(jìn)程可以共享相同的stdio流考润,當(dāng)一個子進(jìn)程并不會導(dǎo)致流關(guān)閉狭园。
由于流是事件的觸發(fā)者,開發(fā)者可以監(jiān)聽子進(jìn)程stdio流中的事件糊治。
與普通進(jìn)程不同唱矛,在子進(jìn)程中,stdout/stderr是可讀流、stdin是可寫流揖赴。從根本上講馆匿,這些流在子進(jìn)程與主進(jìn)程的屬性是相反的。最為重要的燥滑,通過監(jiān)聽data事件渐北,程序可以獲得命令的輸出或執(zhí)行命令時產(chǎn)生的異常信息。
child.stdout.on('data', (data) => {
console.log(`child stdout:\n${data}`);
});
child.stderr.on('data', (data) => {
console.error(`child stderr:\n${data}`);
});
當(dāng)程序執(zhí)行上面spawn函數(shù)铭拧,"pwd"命令的輸出將會打印出來赃蛛。子進(jìn)程將會退出,并返回0搀菩,這說明沒有異常發(fā)生呕臂。
除了可以向spawn函數(shù)衍生出的子進(jìn)程傳遞命令,開發(fā)者還可以向它傳遞命令的參數(shù)肪跋,這個參數(shù)的格式要求是數(shù)組歧蒋。例如下面的find命令:
const child = spawn('find', ['.', '-type', 'f']);
如果命令執(zhí)行的過程中出現(xiàn)異常,child.stderr的data事件被觸發(fā)該事件獲得程序退出code是1(意味著程序出現(xiàn)異常)州既,異常的信息通常是根據(jù)異常的類型和OS系統(tǒng)有所不同谜洽。
由于子進(jìn)程的stdin是可寫流,開發(fā)者可以通過它向子進(jìn)程寫入數(shù)據(jù)吴叶。就像其它的可寫流一樣震肮,pipe方法是使用可寫流最簡單的方式成榜,程序可以將可讀流寫入到可寫流中拂盯。由于主進(jìn)程的stdin是可讀流幽钢,因此可以實現(xiàn)主進(jìn)程向子進(jìn)程穿數(shù)據(jù)。例如:
const { spawn } = require('child_process');
const child = spawn('wc');
process.stdin.pipe(child.stdin)
child.stdout.on('data', (data) => {
console.log(`child stdout:\n${data}`);
});
在上面的例子中逊彭,子進(jìn)程啟動wc命令來計算輸入數(shù)據(jù)的行數(shù)咸灿、字符數(shù)。然后將主進(jìn)程的stdin(可讀流)傳輸給子進(jìn)程的stdin(可寫流)中诫龙。執(zhí)行上面的程序后析显,命令行工具將會開啟輸入模式。當(dāng)輸入組合鍵Ctrl+D后签赃,終止輸入谷异。已經(jīng)輸入的數(shù)據(jù)將會作為wc命令的輸入數(shù)據(jù)源。
開發(fā)者將進(jìn)程的輸出作為另一個進(jìn)程的輸入數(shù)據(jù)源锦聊,實現(xiàn)像linux命令那樣的管道命令歹嘹。例如開發(fā)者將find命令的stdout流,做為wc命令的輸入數(shù)據(jù)源孔庭,實現(xiàn)計量文件夾中的文件數(shù)量:
const { spawn } = require('child_process');
const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);
find.stdout.pipe(wc.stdin);
wc.stdout.on('data', (data) => {
console.log(`Number of files ${data}`);
});
在wc命令后添加參數(shù)-l尺上,實現(xiàn)計算文件的行數(shù)材蛛。上面的程序?qū)Ξ?dāng)前項下所有目錄中所有文件進(jìn)行計數(shù)。
Shell語法和exec函數(shù)
默認(rèn)情況下怎抛,spawn函數(shù)并不會衍生新的shell卑吭,執(zhí)行通過參數(shù)傳遞進(jìn)來的命令。由于不會創(chuàng)建新的shell马绝,這是spawn函數(shù)比exec函數(shù)高效的主要原因豆赏。exec函數(shù)與spawn函數(shù)還有一點主要的區(qū)別,spawn函數(shù)通過流操作命令執(zhí)行的結(jié)果富稻,而exec函數(shù)則將程序執(zhí)行的結(jié)果緩存起來掷邦,最后將緩存的結(jié)果傳給回調(diào)函數(shù)中。
下面通過exec函數(shù)實現(xiàn)find|wx命令的例子:
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}`);
});
因為exec函數(shù)使用shell執(zhí)行命令椭赋,因此開發(fā)者可以直接通過shell句法使用shell管道的特性抚岗。
值得注意,要確保向exec函數(shù)傳遞的OS命令是沒有安全隱患的哪怔。因為用戶只要輸入一些特定的命令就可以實現(xiàn)命令的注入攻擊宣蔚,如:rm -rf ~~。
exec函數(shù)緩存命令的輸出蔓涧,并將輸出的結(jié)果作為回調(diào)函數(shù)的參數(shù)件已,傳遞給回調(diào)函數(shù)笋额。
如果你需要使用shell句法元暴,并且期望命令操作的文件比較小,使用shell句法是一項不錯的選擇兄猩。注意茉盏,exec函數(shù)先將所要返回的數(shù)據(jù)緩存在內(nèi)存中,然后返回枢冤。
如果執(zhí)行命令后得到的數(shù)據(jù)太大鸠姨,spawn函數(shù)將是很不錯的選擇,因為使用spawn函數(shù)會標(biāo)準(zhǔn)的IO對象轉(zhuǎn)換為流淹真。
程序可以通過spawn函數(shù)衍生出繼承父進(jìn)程標(biāo)準(zhǔn)I/O對象的子進(jìn)程讶迁,如果需要,可以在子進(jìn)程中使用shell句法核蘸。下面的代碼就是實現(xiàn)定制子進(jìn)程的代碼:
const child = spawn('find . -type f | wc -l', {
stdio: 'inherit',
shell: true
});
設(shè)置stdion: 'inherit'巍糯,當(dāng)執(zhí)行代碼時,子進(jìn)程將會繼承主進(jìn)程的stdin客扎、stdout和stderr祟峦。主進(jìn)程的process.stdout 流將會觸發(fā)子進(jìn)程的事件處理函數(shù),并在事件處理函數(shù)中立刻輸出結(jié)果徙鱼。
設(shè)置shell: true宅楞,就像exec函數(shù)一樣针姿,程序可以向衍生函數(shù)傳遞shell句法,作為衍生函數(shù)的參數(shù)厌衙。即便這樣距淫,依舊可以利用衍生函數(shù)中流的特性。不得不說這樣是非成粝#酷
除了在spawn衍生函數(shù)的option對象中設(shè)置shell和stdio溉愁,開發(fā)者還有設(shè)置其它的選項。通過cwd屬性設(shè)置程序工作的目錄饲趋。例如下面將程序的工作目錄設(shè)置為下載文件夾拐揭,實現(xiàn)計算對目的文件夾中所有文件計數(shù)的代碼:
const child = spawn('find . -type f | wc -l', {
stdio: 'inherit',
shell: true,
cwd: '/Users/samer/Downloads'
});
使用option對象env屬性,可以設(shè)置對子進(jìn)程可見的環(huán)境變量奕塑。process.env是env屬性的默認(rèn)值,提供對當(dāng)前進(jìn)程環(huán)境的任何命令訪問權(quán)限堂污。開發(fā)者可以設(shè)置env屬性為空對象或子進(jìn)程可見的環(huán)境變量值,實現(xiàn)定制子進(jìn)程可見環(huán)境變量龄砰。
const child = spawn('echo $ANSWER', {
stdio: 'inherit',
shell: true,
env: { ANSWER: 42 },
});
上面的echo命令并不能訪問父進(jìn)程的環(huán)境變量盟猖。由于設(shè)置env屬性值,進(jìn)程沒有訪問$HONE的權(quán)限但是可以訪問$ANSWER换棚。
通過設(shè)置spawn函數(shù)中option對象的detached屬性式镐,可以實現(xiàn)子進(jìn)程完全獨立于父進(jìn)程的調(diào)用。
假設(shè)我們有一個讓事件循環(huán)繁忙的timer.js測試程序:
setTimeout(() => {
// keep the event loop busy
}, 20000);
程序設(shè)置spawn函數(shù)中option對象的detached屬性固蚤,實現(xiàn)在后臺執(zhí)行timer.js程序:
const { spawn } = require('child_process');
const child = spawn('node', ['timer.js'], {
detached: true,
stdio: 'ignore'
});
child.unref();
獨立子進(jìn)程運行在不同的系統(tǒng)娘汞,有不同的行為。在Windows環(huán)境下夕玩,獨立的子進(jìn)程有獨立的控制臺窗口你弦。在Linux環(huán)境下,獨立的子進(jìn)程將會成為新的進(jìn)程組或會話的領(lǐng)導(dǎo)者燎孟。
在獨立的子進(jìn)程中調(diào)用unref函數(shù)禽作,父進(jìn)程可以可以獨立于子進(jìn)程終止運行。這一特性對于下面的場景很適用:子進(jìn)程需要在后臺運行很長時間揩页、子進(jìn)程的stdio流也要獨立于父進(jìn)程旷偿。
上面的示例代碼中,設(shè)置option對象的detached屬性為true 爆侣,獨立的子進(jìn)程在后臺執(zhí)行nodejs代碼(timer.js)萍程。設(shè)置option對象的option對象的stdio屬性為ignore,子進(jìn)程擁有獨立于主進(jìn)程的stdio流累提。這樣就可以實現(xiàn)在子進(jìn)程還是后臺執(zhí)行時尘喝,終止父進(jìn)程。
execFile函數(shù)
如果開發(fā)者不需要使用shell執(zhí)行文件斋陪,execFile函數(shù)是一個不錯的選擇朽褪。execFile函數(shù)與exec函數(shù)很像置吓,但是由于execFile并不會衍生新的shell,這是execFile函數(shù)比exec函數(shù)高效的主要原因缔赠。在Windows環(huán)境下衍锚,諸如.bat和.cmd文件并不能獨立執(zhí)行。但是可以通過exec函數(shù)或是設(shè)置spawn函數(shù)的shell特性執(zhí)行這些文件嗤堰。
*Sync函數(shù)
子進(jìn)程模塊中的spawn函數(shù)戴质,exec函數(shù)和execFile函數(shù)同樣有相應(yīng)同步、阻塞函數(shù)踢匣。它們將會等待子進(jìn)程執(zhí)行完畢后退出告匠。
const {
spawnSync,
execSync,
execFileSync,
} = require('child_process');
這些同步的函數(shù)對于簡化所要執(zhí)行的腳本或處理程序啟動的任務(wù)都非常有用,但是在其它方面要避免使用它們离唬。
fork函數(shù)
fork函數(shù)和spawn函數(shù)在衍生子進(jìn)程時并不相同后专。它們的區(qū)別主要在于:通過fork函數(shù)衍生的子進(jìn)程會建立通信管道,衍生的子進(jìn)程可以通過send函數(shù)向主進(jìn)程發(fā)送信息输莺,主進(jìn)程也可以通過send函數(shù)向子進(jìn)程發(fā)送信息戚哎。下面是示例代碼:
父進(jìn)程代碼:
const { fork } = require('child_process');
const forked = fork('child.js');
forked.on('message', (msg) => {
console.log('Message from child', msg);
});
forked.send({ hello: 'world' });
子進(jìn)程代碼:
process.on('message', (msg) => {
console.log('Message from parent:', msg);
});
let counter = 0;
setInterval(() => {
process.send({ counter: counter++ });
}, 1000);
在父進(jìn)程的程序中,開發(fā)者可以fork文件(這個文件將會通過node命令執(zhí)行)嫂用,然后監(jiān)聽message事件型凳。當(dāng)子進(jìn)程調(diào)用process.send函數(shù)的時,父進(jìn)程的message事件將會被觸發(fā)嘱函。在上面的代碼中甘畅,子進(jìn)程每分鐘都會調(diào)用一次process.send函數(shù)。
當(dāng)從父進(jìn)程向子進(jìn)程傳遞數(shù)據(jù)時实夹,在父進(jìn)程中調(diào)用send函數(shù)后橄浓,子進(jìn)程的message監(jiān)聽事件將會被觸發(fā),從而獲取到父進(jìn)程傳遞的消息亮航。
當(dāng)執(zhí)行上面的父進(jìn)程后,父進(jìn)程將會向子進(jìn)程傳遞對象{hello: 'world'}匀们,然后子進(jìn)程將會把這些父進(jìn)程傳遞的消息打印出來缴淋。同時子進(jìn)程將每隔一分鐘向父進(jìn)程發(fā)送一個遞增的數(shù)字,這些數(shù)字將會在父進(jìn)程控制窗口打印出來泄朴。
讓我們看一個關(guān)于fork更實用的例子:
開發(fā)者在http服務(wù)上開啟兩個api重抖。其中之一是"/compute",在這個api上將會做大量的計算祖灰,計算過程將會占用很長時間钟沛。我們可以用一個for循環(huán)模擬上面的場景:
const http = require('http');
const longComputation = () => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
};
return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
if (req.url === '/compute') {
const sum = longComputation();
return res.end(`Sum is ${sum}`);
} else {
res.end('Ok')
}
});
server.listen(3000);
上面的程序存在一個問題:當(dāng)http服務(wù)"/compute"被請求時,由于for循環(huán)阻塞了http服務(wù)的進(jìn)程局扶,因此http服務(wù)將不能再處理其它api請求恨统。
由于請求的程序需要長期運行叁扫,因此我們可以設(shè)計出很多優(yōu)化代碼性能的方案。其中之一是通過fork函數(shù)衍生出新的子進(jìn)程畜埋,然后將計算的代碼放在子進(jìn)程中運行莫绣,運行結(jié)束后將結(jié)果傳輸給父進(jìn)程。
首先將longComputation函數(shù)封裝在一個獨立的js文件中悠鞍,通過父進(jìn)程的信息指令來執(zhí)行l(wèi)ongComputation函數(shù):
const longComputation = () => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
};
return sum;
};
process.on('message', (msg) => {
const sum = longComputation();
process.send(sum);
});
不需要在主進(jìn)程中做longComputation函數(shù)中的運算对室,而是通過fork函數(shù)衍生出新的子進(jìn)程,然后在子進(jìn)程中計算咖祭,最后通過fork函數(shù)的信息傳遞管道將運算結(jié)果傳回父進(jìn)程中掩宜。
const http = require('http');
const { fork } = require('child_process');
const server = http.createServer();
server.on('request', (req, res) => {
if (req.url === '/compute') {
const compute = fork('compute.js');
compute.send('start');
compute.on('message', sum => {
res.end(`Sum is ${sum}`);
});
} else {
res.end('Ok')
}
});
server.listen(3000);
當(dāng)請求'/compute'時,子進(jìn)程通過process.send函數(shù)將計算的結(jié)果傳回給父進(jìn)程么翰,這樣主進(jìn)程的事件循環(huán)將不再發(fā)生阻塞锭亏。
然而上面代碼的性能受限于程序可以通過fork函數(shù)衍生的進(jìn)程數(shù)量。但是當(dāng)通過http請求時硬鞍,主進(jìn)程并不會阻塞慧瘤。
如果服務(wù)是通過多個fork函數(shù)衍生的子進(jìn)程,Node.js的cluster模塊將會對來自外部的請求固该,做http請求的負(fù)載均衡處理锅减。這就會是我下個主題所要講述的內(nèi)容。
以上就是我關(guān)于這個主題所有的內(nèi)容伐坏,非常感謝你的閱讀怔匣,期待下次再見。