Node多進(jìn)程的實(shí)現(xiàn)方法
我們現(xiàn)在已經(jīng)知道了Node
是單線程運(yùn)行的,這表示潛在的錯(cuò)誤有可能導(dǎo)致線程崩潰,然后進(jìn)程也會(huì)隨著退出,無法做到企業(yè)追求的穩(wěn)定性;另一方面,單進(jìn)程也無法充分多核CPU,這是對(duì)硬件本身的浪費(fèi)。Node
社區(qū)本身也意識(shí)到了這一問題,于是從0.1版本就提供了child_process
模塊,用來提供多進(jìn)程的支持。
1. child_process 模塊
child_process
模塊中包括了很多創(chuàng)建子進(jìn)程的方法,包括fork
、spawn
、exec
、execFile
等等。它們的定義如下:
child_process.exec(command[, options][, callback])
child_process.spawn(command[, args][, options])
child_process.fork(modulePath[, args][, options])
child_process.execFile(file[, args][, options][, callback])
在這4個(gè)API中以spawn
最為基礎(chǔ),因?yàn)槠渌齻€(gè)API或多或少都是借助spawn
實(shí)現(xiàn)的。
2. spawn
spawn
方法的聲明格式如下:
child_process.spawn(command[, args][, options])
spawn
方法會(huì)使用指定的command
來生成一個(gè)新進(jìn)程,執(zhí)行完對(duì)應(yīng)的command
后子進(jìn)程會(huì)自動(dòng)退出。
該命令返回一個(gè)child_process
對(duì)象,這代表開發(fā)者可以通過監(jiān)聽事件來獲得命令執(zhí)行的結(jié)果。
下面我們使用spwan
來執(zhí)行ls
命令:
const spawn = require('child_process').spawn; const ls = spawn('ls', ['-1h', '/usr']); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); }); ls.stderr.on('data', (data) => { console.log('stderr: ', daata.toString()); }); ls.on('close', (code) => { console.log('child process exited with code', code); });
其中spawn
的第一個(gè)參數(shù)雖然是command
,但實(shí)際接收的卻是一個(gè)file
,可以在Linux或者M(jìn)ac OSX上運(yùn)行,這是由于ls
命令也是以可執(zhí)行文件形式存在的。
類似的,在Windows系統(tǒng)下我們可以試著使用dir
命令來實(shí)現(xiàn)功能類似的代碼:
const spawn = require('child_process').spawn; const ls = spawn('dir'); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); });
然而在Windows下執(zhí)行上面代碼會(huì)出現(xiàn)形如Error:spawn dir ENOENT
的錯(cuò)誤。
原因就在于spawn
實(shí)際接收的是一個(gè)文件名而非命令,正確的代碼如下:
const spawn = require('child_process').spawn; const ls = spawn('powershell', ['dir']); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); });
這個(gè)問題的原因與操作系統(tǒng)本身有關(guān),在Linux中,一般都是文件,命令行的命令也不例外,例如ls
命令是一個(gè)名為ls
的可執(zhí)行文件;而在Windows中并沒有名為dir
的可執(zhí)行文件,需要通過cmd
或者powershell
之類的工具提供執(zhí)行環(huán)境。
3. fork
在Linux環(huán)境下,創(chuàng)建一個(gè)新進(jìn)程的本質(zhì)是復(fù)制一個(gè)當(dāng)前的進(jìn)程,當(dāng)用戶調(diào)用 fork
后,操作系統(tǒng)會(huì)先為這個(gè)新進(jìn)程分配空間,然后將父進(jìn)程的數(shù)據(jù)原樣復(fù)制一份過去,父進(jìn)程和子進(jìn)程只有少數(shù)值不同,例如進(jìn)程標(biāo)識(shí)符(PD)。
對(duì)于 Node 來說,父進(jìn)程和子進(jìn)程都有獨(dú)立的內(nèi)存空間和獨(dú)立的 V8 實(shí)例,它們和父進(jìn)程唯一的聯(lián)系是用來進(jìn)程間通信的 IPC Channel。
此外,Node中fork
和 POSIX 系統(tǒng)調(diào)用的不同之處在于Node中的fork
并不會(huì)復(fù)制父進(jìn)程。
Node中的fork
是上面提到的spawn
的一種特例,前面也提到了Node中的fork
并不會(huì)復(fù)制當(dāng)前進(jìn)程。多數(shù)情況下,fork
接收的第一個(gè)參數(shù)是一個(gè)文件名,使用fork("xx.js")
相當(dāng)于在命令行下調(diào)用node xx.js
,并且父進(jìn)程和子進(jìn)程之間可以通過process.send
方法來進(jìn)行通信。
下面我們來看一個(gè)簡(jiǎn)單的栗子:
// master.js 調(diào)用 fork 來創(chuàng)建一個(gè)子進(jìn)程 const child_process = require('child_process'); const worker = child_process.fork('worker.js', ['args1']); worker.on('exit', () => { console.log('child process exit'); }); worker.send({ msg: 'hello child' }); worker.on('message', msg => { console.log('from child: ', msg); }); // worker.js const begin = process.argv[2]; console.log('I am worker ' + begin); process.on('message', msg => { console.log('from parent ', msg); process.exit(); }); process.send({ msg: 'hello parent' });
fork
內(nèi)部會(huì)通過spawn
調(diào)用process.executePath
,即Node
的可執(zhí)行文件地址來生成一個(gè)Node
實(shí)例,然后再用這個(gè)實(shí)例來執(zhí)行fork
方法的modulePath
參數(shù)。
輸出結(jié)果為:
I am worker args1
from parent { msg: 'hello child' }
from child: { msg: 'hello parent' }
child process exit
4. exec和execFile
如果我們開發(fā)一種系統(tǒng),那么對(duì)于不同的模塊可能會(huì)用到不同的技術(shù)來實(shí)現(xiàn),例如 Web服務(wù)器使用 Node ,然后再使用 Java 的消息隊(duì)列提供發(fā)布訂閱服務(wù),這種情況下通常使用進(jìn)程間通信的方式來實(shí)現(xiàn)。
但有時(shí)開發(fā)者不希望使用這么復(fù)雜的方式,或者要調(diào)用的干脆是一個(gè)黑盒系統(tǒng),即無法通過修改源碼來進(jìn)行來實(shí)現(xiàn)進(jìn)程間通信,這時(shí)候往往采用折中的方式,例如通過 shell 來調(diào)用目標(biāo)服務(wù),然后再拿到對(duì)應(yīng)的輸出。
child_process
提供了一個(gè)execFile
方法,它的聲明如下:
child_process.execFile(file, args, options, callback)
說明:
file {String}
要運(yùn)行的程序的文件名
args {Array}
字符串參數(shù)列表
options {Object}
cwd {String}
子進(jìn)程的當(dāng)前工作目錄env {Object}
環(huán)境變量鍵值對(duì)encoding {String}
編碼(默認(rèn)為'utf8'
)timeout {Number}
超時(shí)(默認(rèn)為 0)maxBuffer {Number}
緩沖區(qū)大?。J(rèn)為 200*1024)killSignal {String}
結(jié)束信號(hào)(默認(rèn)為'SIGTERM'
)
callback {Function}
進(jìn)程結(jié)束時(shí)回調(diào)并帶上輸出
error {Error}
stdout {Buffer}
stderr {Buffer}
- 返回:
ChildProcess
對(duì)象
可以看出,execfile
和spawn
在形式上的主要區(qū)別在于execfile
提供了一個(gè)回調(diào)函數(shù),通過這個(gè)回調(diào)函數(shù)可以獲得子進(jìn)程的標(biāo)準(zhǔn)輸出/錯(cuò)誤流。
使用 shell 進(jìn)行跨進(jìn)程調(diào)用長(zhǎng)久以來被認(rèn)為是不穩(wěn)定的,這大概源于人們對(duì)控制臺(tái)不友好的交互體驗(yàn)的恐懼(輸入命令后,很可能長(zhǎng)時(shí)間看不到一個(gè)輸出,盡管后臺(tái)可能在一直運(yùn)算,但在用戶看來和死機(jī)無異)。
在 Linux下執(zhí)行exec
命令后,原有進(jìn)程會(huì)被替換成新的進(jìn)程,進(jìn)而失去對(duì)新進(jìn)程的控制,這代表著新進(jìn)程的狀態(tài)也沒辦法獲取了,此外還有 shell 本身運(yùn)行出現(xiàn)錯(cuò)誤,或者因?yàn)楦鞣N原因出現(xiàn)長(zhǎng)時(shí)間卡頓甚至失去響應(yīng)等情況。
Node.js 提供了比較好的解決方案,timeout
解決了長(zhǎng)時(shí)間卡頓的問題,stdout
和stderr
則提供了標(biāo)準(zhǔn)輸出和錯(cuò)誤輸出,使得子進(jìn)程的狀態(tài)可以被獲取。
5. 各方法之間的比較
5.1 spawn和execFile
為了更好地說明,我們先寫一段簡(jiǎn)單的 C 語言代碼,并將其命名為 example.c
:
#include<stdio.h> int main() { printf("%s", "Hello World!"); return 5; }
使用 gcc
編譯該文件:
gcc example.c -o example
生成名為example
的可執(zhí)行文件,然后將這個(gè)可執(zhí)行文件放到系統(tǒng)環(huán)境變量中,然后打開控制臺(tái),輸入example
,看到最后輸出"Hello World"
。
確保這個(gè)可執(zhí)行文件在任意路徑下都能訪問。
我們分別用spawn
和execfile
來調(diào)用example
文件。
首先是spawn
。
const spawn = require('child_process').spawn; const ls = spawn('example'); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); }); ls.stderr.on('data', (data) => { console.log('stderr: ', daata.toString()); }); ls.on('close', (code) => { console.log('child process exited with code', code); });
程序輸出:
stdout: Hello World!
child process exited with code 5
程序正確打印出了Hello World
,此外還可以看到example
最后的return 5
會(huì)被作為子進(jìn)程結(jié)束的code
被返回。
然后是execFile
。
const exec = require('child_process').exec; const child = exec('example', (error, stdout, stderr) => { if (error) { throw error; } console.log(stdout); });
同樣打印出Hello World
,可見除了調(diào)用形式不同,二者相差不大。
5.2 execFile 和 spawn
在子進(jìn)程的信息交互方面,spawn
使用了流式處理的方式,當(dāng)子進(jìn)程產(chǎn)生數(shù)據(jù)時(shí),主進(jìn)程可以通過監(jiān)聽事件來獲取消息;而exec
是將所有返回的信息放在stdout
里面一次性返回的,也就是該方法的maxBuffer
參數(shù),當(dāng)子進(jìn)程的輸出超過這個(gè)大小時(shí),會(huì)產(chǎn)生一個(gè)錯(cuò)誤。
此外,spawn
有一個(gè)名為shell
的參數(shù):
其類型為一個(gè)布爾值或者字符串,如果這個(gè)值被設(shè)置為true
,,就會(huì)啟動(dòng)一個(gè) shell 來執(zhí)行命令,這個(gè) shell 在 UNIX上是 bin/sh,,在Windows上則是cmd.exe。
5.3 exec 和 execFile
exec
在內(nèi)部也是通過調(diào)用execFile
來實(shí)現(xiàn)的,我們可以從源碼中驗(yàn)證這一點(diǎn),在早期的Node源碼中,exec
命令會(huì)根據(jù)當(dāng)前環(huán)境來初始化一個(gè) shell,,例如 cmd.exe 或者 bin/sh,然后在shell中調(diào)用作為參數(shù)的命令。
通常execFile
的效率要高于exec
,這是因?yàn)?code>execFile沒有啟動(dòng)一個(gè) shell,而是直接調(diào)用 spawn
來實(shí)現(xiàn)的。
6. 進(jìn)程間通信
前面介紹的幾個(gè)用于創(chuàng)建進(jìn)程的方法,都是屬于child_process
的類方法,此外childProcess
類繼承了EventEmitter
,在childProcess
中引入事件給進(jìn)程間通信帶來很大的便利。
childProcess
中定義了如下事件。
Event:'close'
:進(jìn)程的輸入輸出流關(guān)閉時(shí)會(huì)觸發(fā)該事件。
Event:'disconnect'
:通常childProcess.disconnect
調(diào)用后會(huì)觸發(fā)這一事件。
Event:'exit'
:進(jìn)程退出時(shí)觸發(fā)。
Event:'message'
:調(diào)用child_process.send
會(huì)觸發(fā)這一事件
Event:'error'
:該事件的觸發(fā)分為幾種情況:
- 該進(jìn)程無法創(chuàng)建子進(jìn)程。
- 該進(jìn)程無法通過
kill
方法關(guān)閉。 - 無法發(fā)送消息給子進(jìn)程。
Event:'error'
事件無法保證一定會(huì)被觸發(fā),因?yàn)榭赡軙?huì)遇到一些極端情況,例如服務(wù)器斷電等。
上面也提到,childProcess
模塊定義了send
方法,用于進(jìn)程間通信,該方法的聲明如下:
child.send(message[, sendHandle[, options]][, callback])
通過send
方法發(fā)送的消息,可以通過監(jiān)聽message
事件來獲取。
// master.js 父進(jìn)程向子進(jìn)程發(fā)送消息 const child_process = require('child_process'); const worker = child_process.fork('worker.js', ['args1']); worker.on('exit', () => { console.log('child process exit'); }); worker.send({ msg: 'hello child' }); worker.on('message', msg => { console.log('from child: ', msg); }); // worker.js 子進(jìn)程接收父進(jìn)程消息 const begin = process.argv[2]; console.log('I am worker ' + begin); process.on('message', msg => { console.log('from parent ', msg); process.exit(); }); process.send({ msg: 'hello parent' });
send
方法的第一個(gè)參數(shù)類型通常為一個(gè)json
對(duì)象或者原始類型,第二個(gè)參數(shù)是一個(gè)句柄,該句柄可以是一個(gè)net.Socket
或者net.Server
對(duì)象。下面是一個(gè)例子:
//master.js 父進(jìn)程發(fā)送一個(gè) Socket 對(duì)象 const child = require('child_process').fork('worker.js'); // Open up the server object and send the handle. const server = require('net').createServer(); server.on('connection', socket => { socket.end('handled by parent'); }); server.listen(1337, () => { child.send('server', server); }); //worker.js 子進(jìn)程接收 Socket 對(duì)象 process.on('message', (m, server) => { if (m === 'server') { server.on('connection', socket => { socket.end('handled by child'); }); } });
7. Cluster
前面已經(jīng)介紹了child_process
的使用,child_process
的一個(gè)重要使用場(chǎng)景是創(chuàng)建多進(jìn)程服務(wù)來保證服務(wù)穩(wěn)定運(yùn)行。
為了統(tǒng)一 Node 創(chuàng)建多進(jìn)程服務(wù)的方式,Node 在之后的版本中增加了Cluster
模塊,Cluster
可以看作是做了封裝的child_Process
模塊。
Cluster
模塊的一個(gè)顯著優(yōu)點(diǎn)是可以共享同一個(gè)socket
連接,這代表可以使用Cluster
模塊實(shí)現(xiàn)簡(jiǎn)單的負(fù)載均衡。
下面是Cluster
的簡(jiǎn)單栗子:
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log('Master process id is', process.pid); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log('worker process died, id ', worker.process.pid); }); } else { // Worker 可以共享同一個(gè) TCP 連接 // 這里的例子是一個(gè) http 服務(wù)器 http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); console.log('Worker started, process id', process.pid); }
上面是使用Cluster
模塊的一個(gè)簡(jiǎn)單的例子,為了充分利用多核CPU,先調(diào)用OS
模塊的cpus()
方法來獲得CPU的核心數(shù),假設(shè)主機(jī)裝有兩個(gè) CPU,每個(gè)CPU有4個(gè)核,那么總核數(shù)就是8。
在上面的代碼中,Cluster
模塊調(diào)用fork
方法來創(chuàng)建子進(jìn)程,該方法和child_process
中的fork
是同一個(gè)方法。
Cluster
模塊采用的是經(jīng)典的主從模型,由master
進(jìn)程來管理所有的子進(jìn)程,可以使用cluster.isMaster
屬性判斷當(dāng)前進(jìn)程是master
還是worker
,其中主進(jìn)程不負(fù)責(zé)具體的任務(wù)處理,其主要工作是負(fù)責(zé)調(diào)度和管理,上面的代碼中,所有的子進(jìn)程都監(jiān)聽8000端口。
通常情況下,如果多個(gè) Node 進(jìn)程監(jiān)聽同一個(gè)端口時(shí)會(huì)出現(xiàn)Error: listen EADDRINUS
的錯(cuò)誤,而Cluster
模塊能夠讓多個(gè)子進(jìn)程監(jiān)聽同一個(gè)端口的原因是master
進(jìn)程內(nèi)部啟動(dòng)了一個(gè) TCP 服務(wù)器,而真正監(jiān)聽端口的只有這個(gè)服務(wù)器,當(dāng)來自前端的請(qǐng)求觸發(fā)服務(wù)器的connection
事件后,master
會(huì)將對(duì)應(yīng)的socket
句柄發(fā)送給子進(jìn)程。
到此這篇關(guān)于Node多進(jìn)程的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)Node多進(jìn)程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 深入了解 Node的多進(jìn)程服務(wù)實(shí)現(xiàn)
- 詳細(xì)分析Node.js 多進(jìn)程
- Node.js 多進(jìn)程處理CPU密集任務(wù)的實(shí)現(xiàn)
- nodejs基礎(chǔ)之多進(jìn)程實(shí)例詳解
- 深入理解NodeJS 多進(jìn)程和集群
- node.js中TCP Socket多進(jìn)程間的消息推送示例詳解
- Node.js中多進(jìn)程模塊Cluster的介紹與使用
- Nodejs中解決cluster模塊的多進(jìn)程如何共享數(shù)據(jù)問題
- node.js使用cluster實(shí)現(xiàn)多進(jìn)程
相關(guān)文章
socket.io學(xué)習(xí)教程之深入學(xué)習(xí)篇(三)
這篇文章更加深入的給大家介紹了socket.io的相關(guān)資料,之前已經(jīng)介紹了socket.io的基本教程和應(yīng)用,本文更為深入的來介紹下socket.io的使用,需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04詳解Koa中更方便簡(jiǎn)單發(fā)送響應(yīng)的方式
這篇文章主要介紹了詳解Koa中更方便簡(jiǎn)單發(fā)送響應(yīng)的方式,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07基于Node.js構(gòu)建一個(gè)靈活的CLI命令行工具
在軟件開發(fā)中,命令行界面(CLI)工具是必不可少的助手,本文主要介紹了如何使用Node.js構(gòu)建一個(gè)靈活的CLI工具,涵蓋從基礎(chǔ)命令處理到復(fù)雜的交互式問答和遠(yuǎn)程模板下載,需要的可以參考下2024-03-03