nodejs中的子進程,深刻解析child_process模塊和cluster模塊


  node遵循的是單線程單進程的模式,node的單線程是指js的引擎只有一個實例,且在nodejs的主線程中執行,同時node以事件驅動的方式處理IO等異步操做。node的單線程模式,只維持一個主線程,大大減小了線程間切換的開銷。javascript

  可是node的單線程使得在主線程不能進行CPU密集型操做,不然會阻塞主線程。對於CPU密集型操做,在node中經過child_process能夠建立獨立的子進程,父子進程經過IPC通訊,子進程能夠是外部應用也能夠是node子程序,子進程執行後能夠將結果返回給父進程。java

  此外,node的單線程,以單一進程運行,所以沒法利用多核CPU以及其餘資源,爲了調度多核CPU等資源,node還提供了cluster模塊,利用多核CPU的資源,使得能夠經過一串node子進程去處理負載任務,同時保證必定的負載均衡型。本文從node的單線程單進程的理解觸發,介紹了child_process模塊和cluster模塊,本文的結構安排以下:node


  • node中的單線程和單進程
  • node中的child_process模塊實現多進程
  • node中的cluster模塊
  • 總結

原文的地址,在個人博客中:github.com/fortheallli…git

若有幫助,您的star是對我最好的鼓勵~github

1、node中的單線程和單進程

  首先要理解的概念是,node的單線程和單進程的模式。node的單線程於其餘語言的多線程模式相比,減少了線程間切換的開銷,以及在寫node代碼的時候不用考慮鎖以及線程池的問題。node宣稱的單線程模式,比其餘語言更加適合IO密集型操做。那麼一個經典的問題是:web

node是真的單線程的嗎?ajax

提到node,咱們就能夠馬上想到單線程、異步IO、事件驅動等字眼。首先要明確的是node真的是單線程的嗎,若是是單線程的,那麼異步IO,以及定時事件(setTimeout、setInterval等)又是在哪裏被執行的。shell

嚴格來講,node並非單線程的。node中存在着多種線程,包括:安全

  • js引擎執行的線程
  • 定時器線程(setTimeout, setInterval)
  • 異步http線程(ajax) ....

  咱們平時所說的單線程是指node中只有一個js引擎在主線程上運行。其餘異步IO和事件驅動相關的線程經過libuv來實現內部的線程池和線程調度。libv中存在了一個Event Loop,經過Event Loop來切換實現相似於多線程的效果。簡單的來說Event Loop就是維持一個執行棧和一個事件隊列,當前執行棧中的若是發現異步IO以及定時器等函數,就會把這些異步回調函數放入到事件隊列中。當前執行棧執行完成後,從事件隊列中,按照必定的順序執行事件隊列中的異步回調函數。服務器

default

上圖中從執行棧,到事件隊列,最後事件隊列中按照必定的順序執行回調函數,整個過程就是一個簡化版的Event Loop。此外回調函數執行時,一樣會生成一個執行棧,在回調函數裏面還有可能嵌套異步的函數,也就是說執行棧存在着嵌套。

也就是說node中的單線程是指js引擎只在惟一的主線程上運行,其餘的異步操做,也是有獨立的線程去執行,經過libv的Event Loop實現了相似於多線程的上下文切換以及線程池調度。線程是最小的進程,所以node也是單進程的。這樣就解釋了爲何node是單線程和單進程的。

2、node中的child_process模塊實現多進程

  node是單進程的,必然存在一個問題,就是沒法充分利用cpu等資源。node提供了child_process模塊來實現子進程,從而實現一個廣義上的多進程的模式。經過child_process模塊,能夠實現1個主進程,多個子進程的模式,主進程稱爲master進程,子進程又稱工做進程。在子進程中不只能夠調用其餘node程序,也能夠執行非node程序以及shell命令等等,執行完子進程後,以流或者回調的形式返回。

一、child_process模塊提供的API

child_process提供了4個方法,用於新建子進程,這4個方法分別爲spawn、execFile、exec和fork。全部的方法都是異步的,能夠用一張圖來描述這4個方法的區別。

default

上圖能夠展現出這4個方法的區別,咱們也能夠簡要介紹這4中方法的不一樣。

  • spawn : 子進程中執行的是非node程序,提供一組參數後,執行的結果以流的形式返回。

  • execFile:子進程中執行的是非node程序,提供一組參數後,執行的結果以回調的形式返回。

  • exec:子進程執行的是非node程序,傳入一串shell命令,執行後結果以回調的形式返回,與execFile 不一樣的是exec能夠直接執行一串shell命令。

  • fork:子進程執行的是node程序,提供一組參數後,執行的結果以流的形式返回,與spawn不一樣,fork生成的子進程只能執行node應用。接下來的小節將具體的介紹這一些方法。

二、execFile和exec

咱們首先比較execFile和exec的區別,這兩個方法的相同點:

執行的是非node應用,且執行後的結果以回調函數的形式返回。

不一樣點是:

exec是直接執行的一段shell命令,而execFile是執行的一個應用

舉例來講,echo是UNIX系統的一個自帶命令,咱們直接能夠在命令行執行:

echo hello world
複製代碼

結果,在命令行中會打印出hello world.

(1) 經過exec來實現

新建一個main.js文件中,若是要使用exec方法,那麼則在該文件中寫入:

let cp=require('child_process');
cp.exec('echo hello world',function(err,stdout){
  console.log(stdout);
});
複製代碼

執行這個main.js,結果會輸出hello world。咱們發現exec的第一個參數,跟shell命令徹底類似。

(2)經過execFile來實現

let cp=require('child_process');
cp.execFile('echo',['hello','world'],function(err,stdout){
   console.log(stdout);
});
複製代碼

execFile相似於執行了名爲echo的應用,而後傳入參數。execFlie會在process.env.PATH的路徑中依次尋找是否有名爲'echo'的應用,找到後就會執行。默認的process.env.PATH路徑中包含了'usr/local/bin',而這個'usr/local/bin'目錄中就存在了這個名爲'echo'的程序,傳入hello和world兩個參數,執行後返回。

(3)安全性分析

像exec那樣,能夠直接執行一段shell是極爲不安全的,好比有這麼一段shell:

echo hello world;rm -rf
複製代碼

經過exec是能夠直接執行的,rm -rf會刪除當前目錄下的文件。exec正如命令行同樣,執行的等級很高,執行後會出現安全性的問題,而execFile不一樣:

execFile('echo',['hello','world',';rm -rf'])
複製代碼

在傳入參數的同時,會檢測傳入實參執行的安全性,若是存在安全性問題,會拋出異常。除了execFile外,spawn和fork也都不能直接執行shell,所以安全性較高。

三、spawn

spawn一樣是用於執行非node應用,且不能直接執行shell,與execFile相比,spawn執行應用後的結果並非執行完成後,一次性的輸出的,而是以流的形式輸出。對於大批量的數據輸出,經過流的形式能夠介紹內存的使用。

咱們用一個文件的排序和去重來舉例:

default

上述圖片示意圖中,首先讀取的input.txt文件中有acba未經排序的文字,經過sort程序後能夠實現排序功能,輸出爲aabc,最後經過uniq程序能夠去重,獲得abc。咱們能夠用spawn流形式的輸入輸出來實現上述功能:

let cp=require('child_process');
let cat=cp.spawn('cat',['input.txt']);
let sort=cp.spawn('sort');
let uniq=cp.spawn('uniq');

cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);
console.log(process.stdout);
複製代碼

執行後,最後的結果將輸入到process.stdout中。若是input.txt這個文件較大,那麼以流的形式輸入輸出能夠明顯減少內存的佔用,經過設置緩衝區的形式,減少內存佔用的同時也能夠提升輸入輸出的效率。

四、fork

在javascript中,在處理大量計算的任務方面,HTML裏面經過web work來實現,使得任務脫離了主線程。在node中使用了一種內置於父進程和子進程之間的通訊來處理該問題,下降了大數據運行的壓力。node中提供了fork方法,經過fork方法在單獨的進程中執行node程序,而且經過父子間的通訊,子進程接受父進程的信息,並將執行後的結果返回給父進程。

使用fork方法,能夠在父進程和子進程之間開放一個IPC通道,使得不一樣的node進程間能夠進行消息通訊。

在子進程中:

經過process.on('message')和process.send()的機制來接收和發送消息。

在父進程中:

經過child.on('message')和process.send()的機制來接收和發送消息。

具體例子,在child.js中:

process.on('message',function(msg){
   process.send(msg)
})
複製代碼

在parent.js中:

let cp=require('child_process');
let child=cp.fork('./child');
child.on('message',function(msg){
  console.log('got a message is',msg);
});
child.send('hello world');
複製代碼

執行parent.js會在命令行輸出:got a message is hello world

中斷父子間通訊的方式,能夠經過在父進程中調用:

child.disconnect()
複製代碼

來實現斷開父子間IPC通訊。

五、同步執行的子進程

exec、execFile、spawn和fork執行的子進程都是默認異步的,子進程的運行不會阻塞主進程。除此以外,child_process模塊一樣也提供了execFileSync、spawnSync和execSync來實現同步的方式執行子進程。

3、node中的cluster模塊

cluster意爲集成,集成了兩個方面,第一個方面就是集成了child_process.fork方法建立node子進程的方式,第二個方面就是集成了根據多核CPU建立子進程後,自動控制負載均衡的方式。

咱們從官網的例子來看:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主進程 ${process.pid} 正在運行`);

  // 衍生工做進程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工做進程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工做進程能夠共享任何 TCP 鏈接。
  // 在本例子中,共享的是一個 HTTP 服務器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

  console.log(`工做進程 ${process.pid} 已啓動`);
}
複製代碼

最後輸出的結果爲:

$ node server.js
主進程 3596 正在運行
工做進程 4324 已啓動
工做進程 4520 已啓動
工做進程 6056 已啓動
工做進程 5644 已啓動
複製代碼

咱們將master稱爲主進程,而worker進程稱爲工做進程,利用cluster模塊,使用node封裝好的API、IPC通道和調度機能夠很是簡單的建立包括一個master進程下HTTP代理服務器 + 多個worker進程多個HTTP應用服務器的架構。

總結

本文首先介紹了node的單線程和單進程模式,接着從單線程的缺陷觸發,介紹了node中如何實現子進程的方法,對比了child_process模塊中幾種不一樣的子進程生成方案,最後簡單介紹了內置的能夠實現子進程以及CPU進程負載均衡的內置集成模塊cluster。

相關文章
相關標籤/搜索