深入Node.js的進程與子進程:從文檔到實踐


歡迎關注Github倉庫,這是一個自2018年起持續更新的前端&算法開源博客。目前已有node學習、js面試筆記、css3動畫設計、webpack4系列教程、設計模式、劍指offer·js版等多個系列。

倉庫地址:https://github.com/dongyuanxin/blog

進程:process模塊

process 模塊是 nodejs 提供給開發者用來和當前進程交互的工具,它的提供了很多實用的 API。從文檔出發,管中窺豹,進一步認識和學習 process 模塊:

  • 如何處理命令參數?
  • 如何處理工作目錄?
  • 如何處理異常?
  • 如何處理進程退出?
  • process 的標准流對象
  • 深入理解 process.nextTick

如何處理命令參數?

命令行參數指的是 2 個方面:

  • 傳給 node 的參數。例如 node --harmony script.js --version 中, --harmony 就是傳給 node 的參數
  • 傳給進程的參數。例如 node script.js --version --help 中, --version --help 就是傳給進程的參數

它們分別通過 process.argvprocess.execArgv 來獲得。

如何處理工作目錄?

通過process.cwd()可以獲取當前的工作目錄。

通過process.chdir(directory)可以切換當前的工作目錄,失敗后會拋出異常。實踐如下:

function safeChdir(dir) {
    try {
        process.chdir(dir);
        return true;
    } catch (error) {
        return false;
    }
}

如何處理異常?

uncaughtException 事件

Nodejs 可以通過 try-catch 來捕獲異常。如果異常未捕獲,則會一直從底向事件循環冒泡。如是冒泡到事件循環的異常沒被處理,那么就會導致當前進程異常退出。

根據文檔,可以通過監聽 process 的 uncaughtException 事件,來處理未捕獲的異常:

process.on("uncaughtException", (err, origin) => {
    console.log(err.message);
});

const a = 1 / b;
console.log("abc"); // 不會執行

上面的代碼,控制台的輸出是:b is not defined。捕獲了錯誤信息,並且進程以0退出。開發者可以在 uncaughtException 事件中,清除一些已經分配的資源(文件描述符、句柄等),不推薦在其中重啟進程。

unhandledRejection 事件

如果一個 Promise 回調的異常沒有被.catch()捕獲,那么就會觸發 process 的 unhandledRejection 事件:

process.on("unhandledRejection", (err, promise) => {
    console.log(err.message);
});

Promise.reject(new Error("錯誤信息")); // 未被catch捕獲的異常,交由unhandledRejection事件處理

warning 事件

告警不是 Node.js 和 Javascript 錯誤處理流程的正式組成部分。 一旦探測到可能導致應用性能問題,缺陷或安全隱患相關的代碼實踐,Node.js 就可發出告警。

比如前一段代碼中,如果出現未被捕獲的 promise 回調的異常,那么就會觸發 warning 事件。

如何處理進程退出?

process.exit() vs process.exitCode

一個 nodejs 進程,可以通過 process.exit() 來指定退出代碼,直接退出。不推薦直接使用 process.exit(),這會導致事件循環中的任務直接不被處理,以及可能導致數據的截斷和丟失(例如 stdout 的寫入)。

setTimeout(() => {
    console.log("我不會執行");
});

process.exit(0);

正確安全的處理是,設置 process.exitCode,並允許進程自然退出。

setTimeout(() => {
    console.log("我不會執行");
});

process.exitCode = 1;

beforeExit 事件

用於處理進程退出的事件有:beforeExit 事件 和 exit 事件。

當 Node.js 清空其事件循環並且沒有其他工作要安排時,會觸發 beforeExit 事件。例如在退出前需要一些異步操作,那么可以寫在 beforeExit 事件中:

let hasSend = false;
process.on("beforeExit", () => {
    if (hasSend) return; // 避免死循環
setTimeout(<span class="hljs-function" style="line-height: 26px;"><span class="hljs-params" style="line-height: 26px;">()</span> =&gt;</span> {
    <span class="hljs-built_in" style="color: #0086b3; line-height: 26px;">console</span>.log(<span class="hljs-string" style="color: #d14; line-height: 26px;">"mock send data to serve"</span>);
    hasSend = <span class="hljs-literal" style="color: #008080; line-height: 26px;">true</span>;
}, <span class="hljs-number" style="color: #008080; line-height: 26px;">500</span>);

});

console.log(".......");
// 輸出:
// .......
// mock send data to serve

注意:在 beforeExit 事件中如果是異步任務,那么又會被添加到任務隊列。此時,任務隊列完成所有任務后,又回觸發 beforeExit 事件。因此,不處理的話,可能出現死循環的情況。如果是顯式調用 exit(),那么不會觸發此事件。

exit 事件

在 exit 事件中,只能執行同步操作。在調用 'exit' 事件監聽器之后,Node.js 進程將立即退出,從而導致在事件循環中仍排隊的任何其他工作被放棄。

process 的標准流對象

process 提供了 3 個標准流。需要注意的是,它們有些在某些時候是同步阻塞的(請見文檔)。

  • process.stderr:WriteStream 類型, console.error的底層實現,默認對應屏幕
  • process.stdout:WriteStream 類型, console.log的底層實現,默認對應屏幕
  • process.stdin:ReadStream 類型,默認對應鍵盤輸入

下面是基於“生產者-消費者模型”的讀取控制台輸入並且及時輸出的代碼:

process.stdin.setEncoding("utf8");

process.stdin.on("readable", () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
process.stdout.write(&gt;&gt;&gt; <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${chunk}</span>);
}
});

process.stdin.on("end", () => {
process.stdout.write("結束");
});

關於事件的含義,還是請看stream 的文檔

深入理解 process.nextTick

我第一次看到 process.nextTick 的時候是比較懵的,看文檔可以知道,它的用途是:把回調函數作為微任務,放入事件循環的任務隊列中。但這么做的意義是什么呢?

因為 nodejs 並不適合計算密集型的應用,一個進程就一個線程,在當下時間點上,就一個事件在執行。那么,如果我們的事件占用了很多 cpu 時間,那么之后的事件就要等待非常久。所以,nodejs 的一個編程原則是盡量縮短每一個事件的執行事件。process.nextTick 的作用就在這,將一個大的任務分解成多個小的任務。示例代碼如下:

// 被拆分成2個函數執行
function BigThing() {
    doPartThing();
process.nextTick(<span class="hljs-function" style="line-height: 26px;"><span class="hljs-params" style="line-height: 26px;">()</span> =&gt;</span> finishThing());

}

在事件循環中,何時執行 nextTick 注冊的任務呢?請看下面的代碼:

setTimeout(function() {
    console.log("第一個1秒");
    process.nextTick(function() {
        console.log("第一個1秒:nextTick");
    });
}, 1000);

setTimeout(function() {
console.log("第2個1秒");
}, 1000);

console.log("我要輸出1");

process.nextTick(function() {
console.log("nextTick");
});

console.log("我要輸出2");

輸出的結果如下,nextTick 是早於 setTimeout:

我要輸出1
我要輸出2
nextTick
第一個1秒
第一個1秒:nextTick
第21

在瀏覽器端,nextTick 會退化成 setTimeout(callback, 0)。但在 nodejs 中請使用 nextTick 而不是 setTimeout,前者效率更高,並且嚴格來說,兩者創建的事件在任務隊列中順序並不一樣(請看前面的代碼)。

子進程:child_process模塊

掌握 nodejs 的 child_process 模塊能夠極大提高 nodejs 的開發能力,例如主從進程來優化 CPU 計算的問題,多進程開發等等。本文從以下幾個方面介紹 child_process 模塊的使用:

  • 創建子進程
  • 父子進程通信
  • 獨立子進程
  • 進程管道

創建子進程

nodejs 的 child_process 模塊創建子進程的方法:spawn, fork, exec, execFile。它們的關系如下:

  • fork, exec, execFile 都是通過 spawn 來實現的。
  • exec 默認會創建 shell。execFile 默認不會創建 shell,意味着不能使用 I/O 重定向、file glob,但效率更高。
  • spawn、exec、execFile 都有同步版本,可能會造成進程阻塞。

child_process.spawn()的使用:

const { spawn } = require("child_process");
// 返回ChildProcess對象,默認情況下其上的stdio不為null
const ls = spawn("ls", ["-lh"]);

ls.stdout.on("data", data => {
console.log(stdout: <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${data}</span>);
});

ls.stderr.on("data", data => {
console.error(stderr: <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${data}</span>);
});

ls.on("close", code => {
console.log(子進程退出,退出碼 <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${code}</span>);
});

child_process.exec()的使用:

const { exec } = require("child_process");
// 通過回調函數來操作stdio
exec("ls -lh", (err, stdout, stderr) => {
    if (err) {
        console.error(`執行的錯誤: ${err}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
    console.error(`stderr: ${stderr}`);
});

父子進程通信

fork()返回的 ChildProcess 對象,監聽其上的 message 事件,來接受子進程消息;調用 send 方法,來實現 IPC。

parent.js 代碼如下:

const { fork } = require("child_process");
const cp = fork("./sub.js");
cp.on("message", msg => {
    console.log("父進程收到消息:", msg);
});
cp.send("我是父進程");

sub.js 代碼如下:

process.on("message", m => {
    console.log("子進程收到消息:", m);
});

process.send("我是子進程");

運行后結果:

父進程收到消息: 我是子進程
子進程收到消息: 我是父進程

獨立子進程

在正常情況下,父進程一定會等待子進程退出后,才退出。如果想讓父進程先退出,不受到子進程的影響,那么應該:

  • 調用 ChildProcess 對象上的 unref()
  • options.detached 設置為 true
  • 子進程的 stdio 不能是連接到父進程

main.js 代碼如下:

const { spawn } = require("child_process");
const subprocess = spawn(process.argv0, ["sub.js"], {
    detached: true,
    stdio: "ignore"
});

subprocess.unref();

sub.js 代碼如下:

setInterval(() => {}, 1000);

進程管道

options.stdio 選項用於配置在父進程和子進程之間建立的管道。 默認情況下,子進程的 stdin、 stdout 和 stderr 會被重定向到 ChildProcess 對象上相應的 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 流。 這意味着可以通過監聽其上的 data事件,在父進程中獲取子進程的 I/O 。

可以用來實現“重定向”:

const fs = require("fs");
const child_process = require("child_process");

const subprocess = child_process.spawn("ls", {
stdio: [
0, // 使用父進程的 stdin 用於子進程。
"pipe", // 把子進程的 stdout 通過管道傳到父進程 。
fs.openSync("err.out", "w") // 把子進程的 stderr 定向到一個文件。
]
});

也可以用來實現"管道運算符":

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

const ps = spawn("ps", ["ax"]);
const grep = spawn("grep", ["ssh"]);

ps.stdout.on("data", data => {
grep.stdin.write(data);
});

ps.stderr.on("data", err => {
console.error(ps stderr: <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${err}</span>);
});

ps.on("close", code => {
if (code !== 0) {
console.log(ps 進程退出,退出碼 <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${code}</span>);
}
grep.stdin.end();
});

grep.stdout.on("data", data => {
console.log(data.toString());
});

grep.stderr.on("data", data => {
console.error(grep stderr: <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${data}</span>);
});

grep.on("close", code => {
if (code !== 0) {
console.log(grep 進程退出,退出碼 <span class="hljs-subst" style="color: #333; font-weight: normal; line-height: 26px;">${code}</span>);
}
});

參考鏈接

放在最后

  1. 覺得不錯, 幫忙點個贊唄,您的支持是對我最大的激勵
  2. 歡迎我的公眾號: 「心譚博客」,只專注於 前端 + 算法的原創分享


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM