你應該知道的Node.js子進程

文章翻譯自Node.js Child Processes: Everything you need to knownode

如何使用spawn函數、exec函數、execFile函數和for函數linux

child-process.png

Node.js中的非阻塞單線程的特性對單進程任務是很是有用。可是事實上,面對日益複雜的業務邏輯,單個cpu中的單進程所能提供的計算力顯然是不足的。由於不管服務器如何強大,單線程只能夠利用有限的資源。shell

事實上,Node.js運行在單線程上,並不意味着開發者不能利用多進程,固然還有多臺服務器。windows

使用多進程是擴展Node.js程序最佳的方式。Node.js就是爲在多個節點,建立分佈式應用而設計的。這也是取名Node的緣由。可伸縮性已經滲透到平臺中,所以開發不能等到應用程序運行到生命週期後期,在開始思考這個問題。api

請注意,在閱讀本篇文章前你應該理解Node.js事件和Node.js流的相關知識。若是你還沒準備好,我推薦你閱讀下面兩篇文章:數組

Node.js事件驅動 你應該知道的Node.js流緩存

子進程模塊

開發者經過Node的child_process模塊,能夠很容易衍生出子進程。這些子系統能夠經過消息系統實現相互通訊。安全

開發者能夠經過child_process模塊的內部命令,來訪問操做系統。bash

開發者能夠控制子進程的輸入流,監聽其輸出流。開發者一樣能夠控制輸入底層操做系統命令的參數、而且對命令的輸出作任何所須要的改動。因爲命令的輸入與輸出數據均可以被Node.js流處理,所以開發者能夠將一個命令的輸出(就像linux命令那樣)做爲另外一個命令源數據。服務器

注意本文中全部的例子都是基於linux系統,若是你使用的系統時windows系統,你須要將對應的linux命令換成windows命令。

在Node.js中有四種函數建立子進程:spawn()、fork()、exec()和execFile()。

接下來,咱們將會討論這四種函數間的不一樣函數的應用場景。

Spawn(衍生)子進程

Spawan函數能夠衍生出新的子進程,並經過Spwan函數向子進程傳遞命令。例如,經過衍生的子進程,執行"pwd"命令:

const { spawn } = require('child_process');
const child = spawn('pwd');
複製代碼

Node.js程序從child_process模塊析構出spawn函數,向函數傳遞OS命令,並在子進程中執行OS命令。

執行spawn函數的結果是繼承事件接口的子進程實例對象,開發者能夠對它直接註冊事件處理函數。例如開發者對子進程執行結果和子進程退出行爲註冊事件:

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}`);
  });
複製代碼

開發者對子進程還能夠註冊的處理事件有:disconnect、error和message。

  • disconnect事件:當父進程調用child.disconnect函數時觸發
  • error事件:當進程不能衍生或者進程被殺死時觸發
  • close事件:當子進程的stdio流關閉時觸發
  • message事件:當子進程使用process.send()函數時觸發,這個函數主要用於父子進程間的通訊。

每一個子進程都具備標準的stdio流,開發者能夠經過child.stdin、child.stdout和child.stderr操做stdio流。

在子進程中的stdio流關閉時,子進程會觸發close事件。close事件並不徹底等同於exist事件,主要在於子進程能夠共享相同的stdio流,當一個子進程並不會致使流關閉。

因爲流是事件的觸發者,開發者能夠監聽子進程stdio流中的事件。 與普通進程不一樣,在子進程中,stdout/stderr是可讀流、stdin是可寫流。從根本上講,這些流在子進程與主進程的屬性是相反的。最爲重要的,經過監聽data事件,程序能夠得到命令的輸出或執行命令時產生的異常信息。

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

child.stderr.on('data', (data) => {
  console.error(`child stderr:\n${data}`);
});
複製代碼

當程序執行上面spawn函數,"pwd"命令的輸出將會打印出來。子進程將會退出,並返回0,這說明沒有異常發生。

除了能夠向spawn函數衍生出的子進程傳遞命令,開發者還能夠向它傳遞命令的參數,這個參數的格式要求是數組。例以下面的find命令:

const child = spawn('find', ['.', '-type', 'f']);
複製代碼

若是命令執行的過程當中出現異常,child.stderr的data事件被觸發該事件得到程序退出code是1(意味着程序出現異常),異常的信息一般是根據異常的類型和OS系統有所不一樣。

因爲子進程的stdin是可寫流,開發者能夠經過它向子進程寫入數據。就像其它的可寫流同樣,pipe方法是使用可寫流最簡單的方式,程序能夠將可讀流寫入到可寫流中。因爲主進程的stdin是可讀流,所以能夠實現主進程向子進程穿數據。例如:

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}`);
});
複製代碼

在上面的例子中,子進程啓動wc命令來計算輸入數據的行數、字符數。而後將主進程的stdin(可讀流)傳輸給子進程的stdin(可寫流)中。執行上面的程序後,命令行工具將會開啓輸入模式。當輸入組合鍵Ctrl+D後,終止輸入。已經輸入的數據將會做爲wc命令的輸入數據源。

stream-pipe.gif

開發者將進程的輸出做爲另外一個進程的輸入數據源,實現像linux命令那樣的管道命令。例如開發者將find命令的stdout流,作爲wc命令的輸入數據源,實現計量文件夾中的文件數量:

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命令後添加參數-l,實現計算文件的行數。上面的程序將會對當前項下全部目錄中全部文件進行計數。

Shell語法和exec函數

默認狀況下,spawn函數並不會衍生新的shell,執行經過參數傳遞進來的命令。因爲不會建立新的shell,這是spawn函數比exec函數高效的主要緣由。exec函數與spawn函數還有一點主要的區別,spawn函數經過流操做命令執行的結果,而exec函數則將程序執行的結果緩存起來,最後將緩存的結果傳給回調函數中。

下面經過exec函數實現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函數使用shell執行命令,所以開發者能夠直接經過shell句法使用shell管道的特性。

值得注意,要確保向exec函數傳遞的OS命令是沒有安全隱患的。由於用戶只要輸入一些特定的命令就能夠實現命令的注入攻擊,如:rm -rf ~~

exec函數緩存命令的輸出,並將輸出的結果做爲回調函數的參數,傳遞給回調函數。

若是你須要使用shell句法,而且指望命令操做的文件比較小,使用shell句法是一項不錯的選擇。注意,exec函數先將所要返回的數據緩存在內存中,而後返回。

若是執行命令後獲得的數據太大,spawn函數將是很不錯的選擇,由於使用spawn函數會標準的IO對象轉換爲流。

程序能夠經過spawn函數衍生出繼承父進程標準I/O對象的子進程,若是須要,能夠在子進程中使用shell句法。下面的代碼就是實現定製子進程的代碼:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true
});
複製代碼

設置stdion: 'inherit',當執行代碼時,子進程將會繼承主進程的stdin、stdout和stderr。主進程的process.stdout 流將會觸發子進程的事件處理函數,並在事件處理函數中馬上輸出結果。

設置shell: true,就像exec函數同樣,程序能夠向衍生函數傳遞shell句法,做爲衍生函數的參數。即使這樣,依舊能夠利用衍生函數中流的特性。不得不說這樣是很是酷

除了在spawn衍生函數的option對象中設置shell和stdio,開發者還有設置其它的選項。經過cwd屬性設置程序工做的目錄。例以下面將程序的工做目錄設置爲下載文件夾,實現計算對目的文件夾中全部文件計數的代碼:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true,
  cwd: '/Users/samer/Downloads'
});
複製代碼

使用option對象env屬性,能夠設置對子進程可見的環境變量。process.env是env屬性的默認值,提供對當前進程環境的任何命令訪問權限。開發者能夠設置env屬性爲空對象或子進程可見的環境變量值,實現定製子進程可見環境變量。

const child = spawn('echo $ANSWER', {
  stdio: 'inherit',
  shell: true,
  env: { ANSWER: 42 },
});
複製代碼

上面的echo命令並不能訪問父進程的環境變量。因爲設置env屬性值,進程沒有訪問HONE的權限可是能夠訪問ANSWER。

經過設置spawn函數中option對象的detached屬性,能夠實現子進程徹底獨立於父進程的調用。

假設咱們有一個讓事件循環繁忙的timer.js測試程序:

setTimeout(() => {  
  // keep the event loop busy
}, 20000);
複製代碼

程序設置spawn函數中option對象的detached屬性,實如今後臺執行timer.js程序:

const { spawn } = require('child_process');

const child = spawn('node', ['timer.js'], {
  detached: true,
  stdio: 'ignore'
});

child.unref();
複製代碼

獨立子進程運行在不一樣的系統,有不一樣的行爲。在Windows環境下,獨立的子進程有獨立的控制檯窗口。在Linux環境下,獨立的子進程將會成爲新的進程組或會話的領導者。

在獨立的子進程中調用unref函數,父進程能夠能夠獨立於子進程終止運行。這一特性對於下面的場景很適用:子進程須要在後臺運行很長時間、子進程的stdio流也要獨立於父進程。

上面的示例代碼中,設置option對象的detached屬性爲true ,獨立的子進程在後臺執行nodejs代碼(timer.js)。設置option對象的option對象的stdio屬性爲ignore,子進程擁有獨立於主進程的stdio流。這樣就能夠實如今子進程仍是後臺執行時,終止父進程。

independent-stdio.gif

execFile函數

若是開發者不須要使用shell執行文件,execFile函數是一個不錯的選擇。execFile函數與exec函數很像,可是因爲execFile並不會衍生新的shell,這是execFile函數比exec函數高效的主要緣由。在Windows環境下,諸如.bat和.cmd文件並不能獨立執行。可是能夠經過exec函數或是設置spawn函數的shell特性執行這些文件。

*Sync函數

子進程模塊中的spawn函數,exec函數和execFile函數一樣有相應同步、阻塞函數。它們將會等待子進程執行完畢後退出。

const { 
  spawnSync, 
  execSync, 
  execFileSync,
} = require('child_process');
複製代碼

這些同步的函數對於簡化所要執行的腳本或處理程序啓動的任務都很是有用,可是在其它方面要避免使用它們。

fork函數

fork函數和spawn函數在衍生子進程時並不相同。它們的區別主要在於:經過fork函數衍生的子進程會創建通訊管道,衍生的子進程能夠經過send函數向主進程發送信息,主進程也能夠經過send函數向子進程發送信息。下面是示例代碼:

父進程代碼:

const { fork } = require('child_process');

const forked = fork('child.js');

forked.on('message', (msg) => {
  console.log('Message from child', msg);
});

forked.send({ hello: 'world' });
複製代碼

子進程代碼:

process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);
複製代碼

在父進程的程序中,開發者能夠fork文件(這個文件將會經過node命令執行),而後監聽message事件。當子進程調用process.send函數的時,父進程的message事件將會被觸發。在上面的代碼中,子進程每分鐘都會調用一次process.send函數。

當從父進程向子進程傳遞數據時,在父進程中調用send函數後,子進程的message監聽事件將會被觸發,從而獲取到父進程傳遞的消息。

當執行上面的父進程後,父進程將會向子進程傳遞對象{hello: 'world'},而後子進程將會把這些父進程傳遞的消息打印出來。同時子進程將每隔一分鐘向父進程發送一個遞增的數字,這些數字將會在父進程控制窗口打印出來。

fork.gif

讓咱們看一個關於fork更實用的例子:

開發者在http服務上開啓兩個api。其中之一是"/compute",在這個api上將會作大量的計算,計算過程將會佔用很長時間。咱們能夠用一個for循環模擬上面的場景:

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);
複製代碼

上面的程序存在一個問題:當http服務"/compute"被請求時,因爲for循環阻塞了http服務的進程,所以http服務將不能再處理其它api請求。

因爲請求的程序須要長期運行,所以咱們能夠設計出不少優化代碼性能的方案。其中之一是經過fork函數衍生出新的子進程,而後將計算的代碼放在子進程中運行,運行結束後將結果傳輸給父進程。

首先將longComputation函數封裝在一個獨立的js文件中,經過父進程的信息指令來執行longComputation函數:

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);
});
複製代碼

不須要在主進程中作longComputation函數中的運算,而是經過fork函數衍生出新的子進程,而後在子進程中計算,最後經過fork函數的信息傳遞管道將運算結果傳回父進程中。

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);
複製代碼

當請求'/compute'時,子進程經過process.send函數將計算的結果傳回給父進程,這樣主進程的事件循環將再也不發生阻塞。

然而上面代碼的性能受限於程序能夠經過fork函數衍生的進程數量。可是當經過http請求時,主進程並不會阻塞。

若是服務是經過多個fork函數衍生的子進程,Node.js的cluster模塊將會對來自外部的請求,作http請求的負載均衡處理。這就會是我下個主題所要講述的內容。

以上就是我關於這個主題全部的內容,很是感謝你的閱讀,期待下次再見。

相關文章
相關標籤/搜索