溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

NodeJS中多進程和集群的示例分析

發(fā)布時間:2021-09-01 14:39:55 來源:億速云 閱讀:128 作者:小新 欄目:web開發(fā)

小編給大家分享一下NodeJS中多進程和集群的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

進程和線程

“進程” 是計算機系統(tǒng)進行資源分配和調度的基本單位,我們可以理解為計算機每開啟一個任務就會創(chuàng)建至少一個進程來處理,有時會創(chuàng)建多個,如 Chrome 瀏覽器的選項卡,其目的是為了防止一個進程掛掉而應用停止工作,而 “線程” 是程序執(zhí)行流的最小單元,NodeJS 默認是單進程、單線程的,我們將這個進程稱為主進程,也可以通過 child_process 模塊創(chuàng)建子進程實現(xiàn)多進程,我們稱這些子進程為 “工作進程”,并且歸主進程管理,進程之間默認是不能通信的,且所有子進程執(zhí)行任務都是異步的。

spawn 實現(xiàn)多進程

1、spawn 創(chuàng)建子進程

在 NodeJS 中執(zhí)行一個 JS 文件,如果想在這個文件中再同時(異步)執(zhí)行另一個 JS 文件,可以使用 child_process 模塊中的 spawn 來實現(xiàn),spawn 可以幫助我們創(chuàng)建一個子進程,用法如下。

// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js", "--port", "3000"], {
  cwd: path.join(__dirname, "test") // 指定子進程的當前工作目錄
});

// 出現(xiàn)錯誤觸發(fā)
child.on("error", err => console.log(err));

// 子進程退出觸發(fā)
child.on("exit", () => console.log("exit"));

// 子進程關閉觸發(fā)
child.on("close", () => console.log("close"));

// exit
// close

spawn 方法可以幫助我們創(chuàng)建一個子進程,這個子進程就是方法的返回值,spawn 接收以下幾個參數(shù):

  • command:要運行的命令;

  • args:類型為數(shù)組,數(shù)組內第一項為文件名,后面項依次為執(zhí)行文件的命令參數(shù)和值;

  • options:選項,類型為對象,用于指定子進程的當前工作目錄和主進程、子進程的通信規(guī)則等,具體可查看 官方文檔。

error 事件在子進程出錯時觸發(fā),exit 事件在子進程退出時觸發(fā),close 事件在子進程關閉后觸發(fā),在子進程任務結束后 exit 一定會觸發(fā),close 不一定觸發(fā)。

// 文件:~test/sub_process.js
// 打印子進程執(zhí)行 sub_process.js 文件的參數(shù)
console.log(process.argv);

通過上面代碼打印了子進程執(zhí)行時的參數(shù),但是我們發(fā)現(xiàn)主進程窗口并沒有打印,我們希望的是子進程的信息可以反饋給主進程,要實現(xiàn)通信需要在創(chuàng)建子進程時在第三個參數(shù) options 中配置 stdio 屬性定義。

2、spawn 定義輸入、輸出

// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js", "--port", "3000"], {
  cwd: path.join(__dirname, "test") // 指定子進程的當前工作目錄
  // stdin: [process.stdin, process.stdout, process.stderr]
  stdio: [0, 1, 2] // 配置標準輸入、標準輸出、錯誤輸出
});
// C:\Program Files\nodejs\node.exe,g:\process\test\sub_process.js,--port,3000
// 文件:~test/sub_process.js
// 使用主進程的標準輸出,輸出 sub_process.js 文件執(zhí)行的參數(shù)
process.stdout.write(process.argv.toString());

通過上面配置 options 的 stdio 值為數(shù)組,上面的兩種寫法作用相同,都表示子進程和主進程共用了主進程的標準輸入、標準輸出、和錯誤輸出,實際上并沒有實現(xiàn)主進程與子進程的通信,其中 0 和 stdin 代表標準輸入,1 和 stdout 代表標準輸出,2 和 stderr 代表錯誤輸出。

上面這樣的方式只要子進程執(zhí)行 sub_process.js 就會在窗口輸出,如果我們希望是否輸出在主進程里面控制,即實現(xiàn)子進程與主進程的通信,看下面用法。

// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: ["pipe"]
});

child.stdout.on("data", data => console.log(data.toString()));

// hello world
// 文件:~test/sub_process.js
// 子進程執(zhí)行 sub_process.js
process.stdout.write("hello world");

上面將 stdio 內數(shù)組的值配置為 pipe(默認不寫就是 pipe),則通過流的方式實現(xiàn)主進程和子進程的通信,通過子進程的標準輸出(可寫流)寫入,在主進程通過子進程的標準輸出通過 data 事件讀取的流在輸出到窗口(這種寫法很少用),上面都只在主進程中開啟了一個子進程,下面舉一個開啟多個進程的例子。

例子的場景是主進程開啟兩個子進程,先運行子進程 1 傳遞一些參數(shù),子進程 1 將參數(shù)取出返還給主進程,主進程再把參數(shù)傳遞給子進程 2,通過子進程 2 將參數(shù)寫入到文件 param.txt 中,這個過程不代表真實應用場景,主要目的是體會主進程和子進程的通信過程。

// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");

// 創(chuàng)建子進程
let child1 = spawn("node", ["sub_process_1.js", "--port", "3000"], {
  cwd: path.join(__dirname, "test"),
});

let child2 = spawn("node", ["sub_process_2.js"], {
  cwd: path.join(__dirname, "test"),
});


// 讀取子進程 1 寫入的內容,寫入子進程 2
child1.stdout.on("data", data => child2.stdout.write(data.toString));
// 文件:~test/sub_process_1.js
// 獲取 --port 和 3000
process.argv.slice(2).forEach(item => process.stdout.write(item));
// 文件:~test/sub_process_2.js
const fs = require("fs");

// 讀取主進程傳遞的參數(shù)并寫入文件
process.stdout.on("data", data => {
  fs.writeFile("param.txt", data, () => {
    process.exit();
  });
});

有一點需要注意,在子進程 2 寫入文件的時候,由于主進程不知道子進程 2 什么時候寫完,所以主進程會卡住,需要子進程在寫入完成后調用 process.exit 方法退出子進程,子進程退出并關閉后,主進程會隨之關閉。

在我們給 options 配置 stdio 時,數(shù)組內其實可以對標準輸入、標準輸出和錯誤輸出分開配置,默認數(shù)組內為 pipe 時代表三者都為 pipe,分別配置看下面案例。

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: [0, "pipe", 2]
});

// world
// 文件:~test/sub_process.js
console.log("hello");
console.error("world");

上面代碼中對 stderr 實現(xiàn)了默認打印而不通信,對標準輸入實現(xiàn)了通信,還有一種情況,如果希望子進程只是默默的執(zhí)行任務,而在主進程命令窗口什么類型的輸出都禁止,可以在數(shù)組中對應位置給定值 ignore,將上面案例修改如下。

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: [0, "pipe", "ignore"]
});
// 文件:~test/sub_process.js
console.log("hello");
console.error("world");

這次我們發(fā)現(xiàn)無論標準輸出和錯誤輸出都沒有生效,上面這些方式其實是不太方便的,因為輸出有 stdout 和 stderr,在寫法上沒辦法統(tǒng)一,可以通過下面的方式來統(tǒng)一。

3、標準進程通信

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: [0, "pipe", "ignore", "ipc"]
});

child.on("message", data => {
  console.log(data);

  // 回復消息給子進程
  child.send("world");

  // 殺死子進程
  // process.kill(child.pid);
});

// hello
// 文件:~test/sub_process.js
// 給主進程發(fā)送消息
process.send("hello");

// 接收主進程回復的消息
process.on("message", data => {
  console.log(data);

  // 退出子進程
  process.exit();
});

// world

這種方式被稱為標準進程通信,通過給 options 的 stdio 數(shù)組配置 ipc,只要數(shù)組中存在 ipc 即可,一般放在數(shù)組開頭或結尾,配置 ipc 后子進程通過調用自己的 send 方法發(fā)送消息給主進程,主進程中用子進程的 message 事件進行接收,也可以在主進程中接收消息的 message 事件的回調當中,通過子進程的 send 回復消息,并在子進程中用 message 事件進行接收,這樣的編程方式比較統(tǒng)一,更貼近于開發(fā)者的意愿。

4、退出和殺死子進程

上面代碼中子進程在接收到主進程的消息時直接退出,也可以在子進程發(fā)送給消息給主進程時,主進程接收到消息直接殺死子進程,代碼如下。

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: [0, "pipe", "ignore", "ipc"]
});

child.on("message", data => {
  console.log(data);

  // 殺死子進程
  process.kill(child.pid);
});

// hello world
// 文件:~test/sub_process.js
// 給主進程發(fā)送消息
process.send("hello");

從上面代碼我們可以看出,殺死子進程的方法為 process.kill,由于一個主進程可能有多個子進程,所以指定要殺死的子進程需要傳入子進程的 pid 屬性作為 process.kill 的參數(shù)。

{% note warning %}

注意:退出子進程 process.exit 方法是在子進程中操作的,此時 process 代表子進程,殺死子進程 process.kill 是在主進程中操作的,此時 process 代表主進程。

{% endnote %}

5、獨立子進程

我們前面說過,child_process 模塊創(chuàng)建的子進程是被主進程統(tǒng)一管理的,如果主進程掛了,所有的子進程也會受到影響一起掛掉,但其實使用多進程一方面為了提高處理任務的效率,另一方面也是為了當一個進程掛掉時還有其他進程可以繼續(xù)工作,不至于整個應用掛掉,這樣的例子非常多,比如 Chrome 瀏覽器的選項卡,比如 VSCode 編輯器運行時都會同時開啟多個進程同時處理任務,其實在 spawn 創(chuàng)建子進程時,也可以實現(xiàn)子進程的獨立,即子進程不再受主進程的控制和影響。

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");

// 創(chuàng)建子進程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: "ignore",
  detached: true
});

// 與主進程斷絕關系
child.unref();
// 文件:~test/sub_process.js
const fs = require("fs");

setInterval(() => {
  fs.appendFileSync("test.txt", "hello");
});

要想創(chuàng)建的子進程獨立,需要在創(chuàng)建子進程時配置 detached 參數(shù)為 true,表示該子進程不受控制,還需調用子進程的 unref 方法與主進程斷絕關系,但是僅僅這樣子進程可能還是會受主進程的影響,要想子進程完全獨立需要保證子進程一定不能和主進程共用標準輸入、標準輸出和錯誤輸出,也就是 stdio 必須設置為 ignore,這也就代表著獨立的子進程是不能和主進程進行標準進程通信,即不能設置 ipc。

fork 實現(xiàn)多進程

1、fork 的使用

fork 也是 child_process 模塊的一個方法,與 spawn 類似,是在 spawn 的基礎上又做了一層封裝,我們看一個 fork 使用的例子。

// 文件:process.js
const fork = require("child_process");
const path = require("path");

// 創(chuàng)建子進程
let child = fork("sub_process.js", ["--port", "3000"], {
  cwd: path.join(__dirname, "test"),
  silent: true
});

child.send("hello world");
// 文件:~test/sub_process.js
// 接收主進程發(fā)來的消息
process.on("message", data => console.log(data));

fork 的用法與 spawn 相比有所改變,第一個參數(shù)是子進程執(zhí)行文件的名稱,第二個參數(shù)為數(shù)組,存儲執(zhí)行時的參數(shù)和值,第三個參數(shù)為 options,其中使用 slilent 屬性替代了 spawn 的 stdio,當 silent 為 true 時,此時主進程與子進程的所有非標準通信的操作都不會生效,包括標準輸入、標準輸出和錯誤輸出,當設為 false 時可正常輸出,返回值依然為一個子進程。

fork 創(chuàng)建的子進程可以直接通過 send 方法和監(jiān)聽 message 事件與主進程進行通信。

2、fork 的原理

其實 fork 的原理非常簡單,只是在子進程模塊 child_process 上掛了一個 fork 方法,而在該方法內調用 spawn 并將 spawn 返回的子進程作為返回值返回,下面進行簡易實現(xiàn)。

// 文件:fork.js
const childProcess = require("child_process");
const path = require("path");

// 封裝原理
childProcess.fork = function (modulePath, args, options) {
  let stdio = options.silent ? ["ignore", "ignore", "ignore", "ipc"] : [0, 1, 2, "ipc"];
  return childProcess.spawn("node", [modulePath, ...args], {
    ...options,
    stdio
  });
}

// 創(chuàng)建子進程
let child = fork("sub_process.js", ["--port", "3000"], {
  cwd: path.join(__dirname, "test"),
  silent: false
});

// 向子進程發(fā)送消息
child.send("hello world");
// 文件:~test/sub_process.js
// 接收主進程發(fā)來的消息
process.on("message", data => console.log(data));

// hello world

spawn 中的有一些 fork 沒有傳的參數(shù)(如使用 node 執(zhí)行文件),都在內部調用 spawn 時傳遞默認值或將默認參數(shù)與 fork 傳入的參數(shù)進行整合,著重處理了 spawn 沒有的參數(shù) silent,其實就是處理成了 spawn 的 stdio 參數(shù)兩種極端的情況(默認使用 ipc 通信),封裝 fork 就是讓我們能更方便的創(chuàng)建子進程,可以更少的傳參。

execFile 和 exec 實現(xiàn)多進程

execFile 和 exec 是 child_process 模塊的兩個方法,execFile 是基于 spawn 封裝的,而 exec 是基于 execFile 封裝的,這兩個方法用法大同小異,execFile 可以直接創(chuàng)建子進程進行文件操作,而 exec 可以直接開啟子進程執(zhí)行命令,常見的應用場景如 http-server 以及 weboack-dev-server 等命令行工具在啟動本地服務時自動打開瀏覽器。

// execFile 和 exec
const { execFile, exec } = require("child_process");

let execFileChild = execFile("node", ["--version"], (err, stdout, stderr) => {
  if (error) throw error;
  console.log(stdout);
  console.log(stderr);
});

let execChild = exec("node --version", (err, stdout, stderr) => {
  if (err) throw err;
  console.log(stdout);
  console.log(stderr);
});

exec 與 execFile 的區(qū)別在于傳參,execFile 第一個參數(shù)為文件的可執(zhí)行路徑或命令,第二個參數(shù)為命令的參數(shù)集合(數(shù)組),第三個參數(shù)為 options,最后一個參數(shù)為回調函數(shù),回調函數(shù)的形參為錯誤、標準輸出和錯誤輸出。

exec 在傳參上將 execFile 的前兩個參數(shù)進行了整合,也就是命令與命令參數(shù)拼接成字符串作為第一參數(shù),后面的參數(shù)都與 execFile 相同。

cluster 集群

開啟進程需要消耗內存,所以開啟進程的數(shù)量要適合,合理運用多進程可以大大提高效率,如 Webpack 對資源進行打包,就開啟了多個進程同時進行,大大提高了打包速度,集群也是多進程重要的應用之一,用多個進程同時監(jiān)聽同一個服務,一般開啟進程的數(shù)量跟 CPU 核數(shù)相同為好,此時多個進程監(jiān)聽的服務會根據(jù)請求壓力分流處理,也可以通過設置每個子進程處理請求的數(shù)量來實現(xiàn) “負載均衡”。

1、使用 ipc 實現(xiàn)集群

ipc 標準進程通信使用 send 方法發(fā)送消息時第二個參數(shù)支持傳入一個服務,必須是 http 服務或者 tcp 服務,子進程通過 message 事件進行接收,回調的參數(shù)分別對應發(fā)送的參數(shù),即第一個參數(shù)為消息,第二個參數(shù)為服務,我們就可以在子進程創(chuàng)建服務并對主進程的服務進行監(jiān)聽和操作(listen 除了可以監(jiān)聽端口號也可以監(jiān)聽服務),便實現(xiàn)了集群,代碼如下。

// 文件:server.js
const os = require("os"); // os 模塊用于獲取系統(tǒng)信息
const http = require("http");
const path = require("path");
const { fork } = rquire("child_process");

// 創(chuàng)建服務
const server = createServer((res, req) => {
  res.end("hello");
}).listen(3000);

// 根據(jù) CPU 個數(shù)創(chuàng)建子進程
os.cpus().forEach(() => {
  fork("child_server.js", {
    cwd: path.join(__dirname);
  }).send("server", server);
});
// 文件:child_server.js
const http = require("http");

// 接收來自主進程發(fā)來的服務
process.on("message", (data, server) => {
  http.createServer((req, res) => {
    res.end(`child${process.pid}`);
  }).listen(server); // 子進程共用主進程的服務
});

上面代碼中由主進程處理的請求會返回 hello,由子進程處理的請求會返回 child 加進程的 pid 組成的字符串。

2、使用 cluster 實現(xiàn)集群

cluster 模塊是 NodeJS 提供的用來實現(xiàn)集群的,他將 child_process 創(chuàng)建子進程的方法集成進去,實現(xiàn)方式要比使用 ipc 更簡潔。

// 文件:cluster.js
const cluster = require("cluster");
const http = require("http");
const os = require("os");

// 判斷當前執(zhí)行的進程是否為主進程,為主進程則創(chuàng)建子進程,否則用子進程監(jiān)聽服務
if (cluster.isMaster) {
  // 創(chuàng)建子進程
  os.cpus().forEach(() => cluster.fork());
} else {
  // 創(chuàng)建并監(jiān)聽服務
  http.createServer((req, res) => {
    res.end(`child${process.pid}`);
  }).listen(3000);
}

上面代碼既會執(zhí)行 if 又會執(zhí)行 else,這看似很奇怪,但其實不是在同一次執(zhí)行的,主進程執(zhí)行時會通過 cluster.fork 創(chuàng)建子進程,當子進程被創(chuàng)建會將該文件再次執(zhí)行,此時則會執(zhí)行 else 中對服務的監(jiān)聽,還有另一種用法將主進程和子進程執(zhí)行的代碼拆分開,邏輯更清晰,用法如下。

// 文件:cluster.js
const cluster = require("cluster");
const path = require("path");
const os = require("os");

// 設置子進程讀取文件的路徑
cluster.setupMaster({
  exec: path.join(__dirname, "cluster-server.js")
});

// 創(chuàng)建子進程
os.cpus().forEach(() => cluster.fork());
// 文件:cluster-server.js
const http = require("http");

// 創(chuàng)建并監(jiān)聽服務
http.createServer((req, res) => {
  res.end(`child${process.pid}`);
}).listen(3000);

通過 cluster.setupMaster 設置子進程執(zhí)行文件以后,就可以將主進程和子進程的邏輯拆分開,在實際的開發(fā)中這樣的方式也是最常用的,耦合度低,可讀性好,更符合開發(fā)的原則。

以上是“NodeJS中多進程和集群的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI