1. 程式人生 > 實用技巧 >nodejs中的子程序,深入解析child_process模組和cluster模組

nodejs中的子程序,深入解析child_process模組和cluster模組

Node.js的程序管理

  node遵循的是單執行緒單程序的模式,node的單執行緒是指js的引擎只有一個例項,且在nodejs的主執行緒中執行,同時node以事件驅動的方式處理IO等非同步操作。node的單執行緒模式,只維持一個主執行緒,大大減少了執行緒間切換的開銷。

  但是node的單執行緒使得在主執行緒不能進行CPU密集型操作,否則會阻塞主執行緒。對於CPU密集型操作,在node中通過child_process可以建立獨立的子程序,父子程序通過IPC通訊,子程序可以是外部應用也可以是node子程式,子程序執行後可以將結果返回給父程序。

  此外,node的單執行緒,以單一程序執行,因此無法利用多核CPU以及其他資源,為了排程多核CPU等資源,node還提供了cluster模組,利用多核CPU的資源,使得可以通過一串node子程序去處理負載任務,同時保證一定的負載均衡型。本文從node的單執行緒單程序的理解觸發,介紹了child_process模組和cluster模組,本文的結構安排如下:


  • node中的單執行緒和單程序
  • node中的child_process模組實現多程序
  • node中的cluster模組
  • 總結

原文的地址,在我的部落格中:https://github.com/forthealll...

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

一、node中的單執行緒和單程序

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

node是真的單執行緒的嗎?

提到node,我們就可以立刻想到單執行緒、非同步IO、事件驅動等字眼。首先要明確的是node真的是單執行緒的嗎,如果是單執行緒的,那麼非同步IO,以及定時事件(setTimeout、setInterval等)又是在哪裡被執行的。

嚴格來說,node並不是單執行緒的。node中存在著多種執行緒,包括:

  • js引擎執行的執行緒
  • 定時器執行緒(setTimeout, setInterval)
  • 非同步http執行緒(ajax)

....

  我們平時所說的單執行緒是指node中只有一個js引擎在主執行緒上執行。其他非同步IO和事件驅動相關的執行緒通過libuv來實現內部的執行緒池和執行緒排程。libv中存在了一個Event Loop,通過Event Loop來切換實現類似於多執行緒的效果。簡單的來講Event Loop就是維持一個執行棧和一個事件佇列,當前執行棧中的如果發現非同步IO以及定時器等函式,就會把這些非同步回撥函式放入到事件佇列中。當前執行棧執行完成後,從事件佇列中,按照一定的順序執行事件佇列中的非同步回撥函式。

上圖中從執行棧,到事件佇列,最後事件佇列中按照一定的順序執行回撥函式,整個過程就是一個簡化版的Event Loop。此外回撥函式執行時,同樣會生成一個執行棧,在回撥函式裡面還有可能巢狀非同步的函式,也就是說執行棧存在著巢狀。

也就是說node中的單執行緒是指js引擎只在唯一的主執行緒上執行,其他的非同步操作,也是有獨立的執行緒去執行,通過libv的Event Loop實現了類似於多執行緒的上下文切換以及執行緒池排程。執行緒是最小的程序,因此node也是單程序的。這樣就解釋了為什麼node是單執行緒和單程序的。

二、node中的child_process模組實現多程序

  node是單程序的,必然存在一個問題,就是無法充分利用cpu等資源。node提供了child_process模組來實現子程序,從而實現一個廣義上的多程序的模式。通過child_process模組,可以實現1個主程序,多個子程序的模式,主程序稱為master程序,子程序又稱工作程序。在子程序中不僅可以呼叫其他node程式,也可以執行非node程式以及shell命令等等,執行完子程序後,以流或者回調的形式返回。

1、child_process模組提供的API

child_process提供了4個方法,用於新建子程序,這4個方法分別為spawn、execFile、exec和fork。所有的方法都是非同步的,可以用一張圖來描述這4個方法的區別。

上圖可以展示出這4個方法的區別,我們也可以簡要介紹這4中方法的不同。

  • spawn: 子程序中執行的是非node程式,提供一組引數後,執行的結果以流的形式返回。
  • execFile:子程序中執行的是非node程式,提供一組引數後,執行的結果以回撥的形式返回。
  • exec:子程序執行的是非node程式,傳入一串shell命令,執行後結果以回撥的形式返回,與execFile
    不同的是exec可以直接執行一串shell命令。
  • fork:子程序執行的是node程式,提供一組引數後,執行的結果以流的形式返回,與spawn不同,fork生成的子程序只能執行node應用。接下來的小節將具體的介紹這一些方法。

2、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,因此安全性較高。

3、spawn

spawn同樣是用於執行非node應用,且不能直接執行shell,與execFile相比,spawn執行應用後的結果並不是執行完成後,一次性的輸出的,而是以流的形式輸出。對於大批量的資料輸出,通過流的形式可以介紹記憶體的使用。

我們用一個檔案的排序和去重來舉例:

上述圖片示意圖中,首先讀取的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這個檔案較大,那麼以流的形式輸入輸出可以明顯減小記憶體的佔用,通過設定緩衝區的形式,減小記憶體佔用的同時也可以提高輸入輸出的效率。

4、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通訊。

5、同步執行的子程序

exec、execFile、spawn和fork執行的子程序都是預設非同步的,子程序的執行不會阻塞主程序。除此之外,child_process模組同樣也提供了execFileSync、spawnSync和execSync來實現同步的方式執行子程序。

三、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。

來源:https://segmentfault.com/a/1190000016169207