您好,登錄后才能下訂單哦!
小編給大家分享一下NodeJS中多進程和集群的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
進程和線程
“進程” 是計算機系統進行資源分配和調度的基本單位,我們可以理解為計算機每開啟一個任務就會創建至少一個進程來處理,有時會創建多個,如 Chrome 瀏覽器的選項卡,其目的是為了防止一個進程掛掉而應用停止工作,而 “線程” 是程序執行流的最小單元,NodeJS 默認是單進程、單線程的,我們將這個進程稱為主進程,也可以通過 child_process 模塊創建子進程實現多進程,我們稱這些子進程為 “工作進程”,并且歸主進程管理,進程之間默認是不能通信的,且所有子進程執行任務都是異步的。
spawn 實現多進程
1、spawn 創建子進程
在 NodeJS 中執行一個 JS 文件,如果想在這個文件中再同時(異步)執行另一個 JS 文件,可以使用 child_process 模塊中的 spawn 來實現,spawn 可以幫助我們創建一個子進程,用法如下。
// 文件:process.js const { spawn } = require("child_process"); const path = require("path"); // 創建子進程 let child = spawn("node", ["sub_process.js", "--port", "3000"], { cwd: path.join(__dirname, "test") // 指定子進程的當前工作目錄 }); // 出現錯誤觸發 child.on("error", err => console.log(err)); // 子進程退出觸發 child.on("exit", () => console.log("exit")); // 子進程關閉觸發 child.on("close", () => console.log("close")); // exit // close
spawn 方法可以幫助我們創建一個子進程,這個子進程就是方法的返回值,spawn 接收以下幾個參數:
command:要運行的命令;
args:類型為數組,數組內第一項為文件名,后面項依次為執行文件的命令參數和值;
options:選項,類型為對象,用于指定子進程的當前工作目錄和主進程、子進程的通信規則等,具體可查看 官方文檔。
error 事件在子進程出錯時觸發,exit 事件在子進程退出時觸發,close 事件在子進程關閉后觸發,在子進程任務結束后 exit 一定會觸發,close 不一定觸發。
// 文件:~test/sub_process.js // 打印子進程執行 sub_process.js 文件的參數 console.log(process.argv);
通過上面代碼打印了子進程執行時的參數,但是我們發現主進程窗口并沒有打印,我們希望的是子進程的信息可以反饋給主進程,要實現通信需要在創建子進程時在第三個參數 options 中配置 stdio 屬性定義。
2、spawn 定義輸入、輸出
// 文件:process.js const { spawn } = require("child_process"); const path = require("path"); // 創建子進程 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 文件執行的參數 process.stdout.write(process.argv.toString());
通過上面配置 options 的 stdio 值為數組,上面的兩種寫法作用相同,都表示子進程和主進程共用了主進程的標準輸入、標準輸出、和錯誤輸出,實際上并沒有實現主進程與子進程的通信,其中 0 和 stdin 代表標準輸入,1 和 stdout 代表標準輸出,2 和 stderr 代表錯誤輸出。
上面這樣的方式只要子進程執行 sub_process.js 就會在窗口輸出,如果我們希望是否輸出在主進程里面控制,即實現子進程與主進程的通信,看下面用法。
// 文件:process.js const { spawn } = require("child_process"); const path = require("path"); // 創建子進程 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 // 子進程執行 sub_process.js process.stdout.write("hello world");
上面將 stdio 內數組的值配置為 pipe(默認不寫就是 pipe),則通過流的方式實現主進程和子進程的通信,通過子進程的標準輸出(可寫流)寫入,在主進程通過子進程的標準輸出通過 data 事件讀取的流在輸出到窗口(這種寫法很少用),上面都只在主進程中開啟了一個子進程,下面舉一個開啟多個進程的例子。
例子的場景是主進程開啟兩個子進程,先運行子進程 1 傳遞一些參數,子進程 1 將參數取出返還給主進程,主進程再把參數傳遞給子進程 2,通過子進程 2 將參數寫入到文件 param.txt 中,這個過程不代表真實應用場景,主要目的是體會主進程和子進程的通信過程。
// 文件:process.js const { spawn } = require("child_process"); const path = require("path"); // 創建子進程 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"); // 讀取主進程傳遞的參數并寫入文件 process.stdout.on("data", data => { fs.writeFile("param.txt", data, () => { process.exit(); }); });
有一點需要注意,在子進程 2 寫入文件的時候,由于主進程不知道子進程 2 什么時候寫完,所以主進程會卡住,需要子進程在寫入完成后調用 process.exit 方法退出子進程,子進程退出并關閉后,主進程會隨之關閉。
在我們給 options 配置 stdio 時,數組內其實可以對標準輸入、標準輸出和錯誤輸出分開配置,默認數組內為 pipe 時代表三者都為 pipe,分別配置看下面案例。
// 文件:process.js const { spawn } = require("spawn"); const path = require("path"); // 創建子進程 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 實現了默認打印而不通信,對標準輸入實現了通信,還有一種情況,如果希望子進程只是默默的執行任務,而在主進程命令窗口什么類型的輸出都禁止,可以在數組中對應位置給定值 ignore,將上面案例修改如下。
// 文件:process.js const { spawn } = require("spawn"); const path = require("path"); // 創建子進程 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");
這次我們發現無論標準輸出和錯誤輸出都沒有生效,上面這些方式其實是不太方便的,因為輸出有 stdout 和 stderr,在寫法上沒辦法統一,可以通過下面的方式來統一。
3、標準進程通信
// 文件:process.js const { spawn } = require("spawn"); const path = require("path"); // 創建子進程 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 // 給主進程發送消息 process.send("hello"); // 接收主進程回復的消息 process.on("message", data => { console.log(data); // 退出子進程 process.exit(); }); // world
這種方式被稱為標準進程通信,通過給 options 的 stdio 數組配置 ipc,只要數組中存在 ipc 即可,一般放在數組開頭或結尾,配置 ipc 后子進程通過調用自己的 send 方法發送消息給主進程,主進程中用子進程的 message 事件進行接收,也可以在主進程中接收消息的 message 事件的回調當中,通過子進程的 send 回復消息,并在子進程中用 message 事件進行接收,這樣的編程方式比較統一,更貼近于開發者的意愿。
4、退出和殺死子進程
上面代碼中子進程在接收到主進程的消息時直接退出,也可以在子進程發送給消息給主進程時,主進程接收到消息直接殺死子進程,代碼如下。
// 文件:process.js const { spawn } = require("spawn"); const path = require("path"); // 創建子進程 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 // 給主進程發送消息 process.send("hello");
從上面代碼我們可以看出,殺死子進程的方法為 process.kill,由于一個主進程可能有多個子進程,所以指定要殺死的子進程需要傳入子進程的 pid 屬性作為 process.kill 的參數。
{% note warning %}
注意:退出子進程 process.exit 方法是在子進程中操作的,此時 process 代表子進程,殺死子進程 process.kill 是在主進程中操作的,此時 process 代表主進程。
{% endnote %}
5、獨立子進程
我們前面說過,child_process 模塊創建的子進程是被主進程統一管理的,如果主進程掛了,所有的子進程也會受到影響一起掛掉,但其實使用多進程一方面為了提高處理任務的效率,另一方面也是為了當一個進程掛掉時還有其他進程可以繼續工作,不至于整個應用掛掉,這樣的例子非常多,比如 Chrome 瀏覽器的選項卡,比如 VSCode 編輯器運行時都會同時開啟多個進程同時處理任務,其實在 spawn 創建子進程時,也可以實現子進程的獨立,即子進程不再受主進程的控制和影響。
// 文件:process.js const { spawn } = require("spawn"); const path = require("path"); // 創建子進程 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"); });
要想創建的子進程獨立,需要在創建子進程時配置 detached 參數為 true,表示該子進程不受控制,還需調用子進程的 unref 方法與主進程斷絕關系,但是僅僅這樣子進程可能還是會受主進程的影響,要想子進程完全獨立需要保證子進程一定不能和主進程共用標準輸入、標準輸出和錯誤輸出,也就是 stdio 必須設置為 ignore,這也就代表著獨立的子進程是不能和主進程進行標準進程通信,即不能設置 ipc。
fork 實現多進程
1、fork 的使用
fork 也是 child_process 模塊的一個方法,與 spawn 類似,是在 spawn 的基礎上又做了一層封裝,我們看一個 fork 使用的例子。
// 文件:process.js const fork = require("child_process"); const path = require("path"); // 創建子進程 let child = fork("sub_process.js", ["--port", "3000"], { cwd: path.join(__dirname, "test"), silent: true }); child.send("hello world");
// 文件:~test/sub_process.js // 接收主進程發來的消息 process.on("message", data => console.log(data));
fork 的用法與 spawn 相比有所改變,第一個參數是子進程執行文件的名稱,第二個參數為數組,存儲執行時的參數和值,第三個參數為 options,其中使用 slilent 屬性替代了 spawn 的 stdio,當 silent 為 true 時,此時主進程與子進程的所有非標準通信的操作都不會生效,包括標準輸入、標準輸出和錯誤輸出,當設為 false 時可正常輸出,返回值依然為一個子進程。
fork 創建的子進程可以直接通過 send 方法和監聽 message 事件與主進程進行通信。
2、fork 的原理
其實 fork 的原理非常簡單,只是在子進程模塊 child_process 上掛了一個 fork 方法,而在該方法內調用 spawn 并將 spawn 返回的子進程作為返回值返回,下面進行簡易實現。
// 文件: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 }); } // 創建子進程 let child = fork("sub_process.js", ["--port", "3000"], { cwd: path.join(__dirname, "test"), silent: false }); // 向子進程發送消息 child.send("hello world");
// 文件:~test/sub_process.js // 接收主進程發來的消息 process.on("message", data => console.log(data)); // hello world
spawn 中的有一些 fork 沒有傳的參數(如使用 node 執行文件),都在內部調用 spawn 時傳遞默認值或將默認參數與 fork 傳入的參數進行整合,著重處理了 spawn 沒有的參數 silent,其實就是處理成了 spawn 的 stdio 參數兩種極端的情況(默認使用 ipc 通信),封裝 fork 就是讓我們能更方便的創建子進程,可以更少的傳參。
execFile 和 exec 實現多進程
execFile 和 exec 是 child_process 模塊的兩個方法,execFile 是基于 spawn 封裝的,而 exec 是基于 execFile 封裝的,這兩個方法用法大同小異,execFile 可以直接創建子進程進行文件操作,而 exec 可以直接開啟子進程執行命令,常見的應用場景如 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 的區別在于傳參,execFile 第一個參數為文件的可執行路徑或命令,第二個參數為命令的參數集合(數組),第三個參數為 options,最后一個參數為回調函數,回調函數的形參為錯誤、標準輸出和錯誤輸出。
exec 在傳參上將 execFile 的前兩個參數進行了整合,也就是命令與命令參數拼接成字符串作為第一參數,后面的參數都與 execFile 相同。
cluster 集群
開啟進程需要消耗內存,所以開啟進程的數量要適合,合理運用多進程可以大大提高效率,如 Webpack 對資源進行打包,就開啟了多個進程同時進行,大大提高了打包速度,集群也是多進程重要的應用之一,用多個進程同時監聽同一個服務,一般開啟進程的數量跟 CPU 核數相同為好,此時多個進程監聽的服務會根據請求壓力分流處理,也可以通過設置每個子進程處理請求的數量來實現 “負載均衡”。
1、使用 ipc 實現集群
ipc 標準進程通信使用 send 方法發送消息時第二個參數支持傳入一個服務,必須是 http 服務或者 tcp 服務,子進程通過 message 事件進行接收,回調的參數分別對應發送的參數,即第一個參數為消息,第二個參數為服務,我們就可以在子進程創建服務并對主進程的服務進行監聽和操作(listen 除了可以監聽端口號也可以監聽服務),便實現了集群,代碼如下。
// 文件:server.js const os = require("os"); // os 模塊用于獲取系統信息 const http = require("http"); const path = require("path"); const { fork } = rquire("child_process"); // 創建服務 const server = createServer((res, req) => { res.end("hello"); }).listen(3000); // 根據 CPU 個數創建子進程 os.cpus().forEach(() => { fork("child_server.js", { cwd: path.join(__dirname); }).send("server", server); });
// 文件:child_server.js const http = require("http"); // 接收來自主進程發來的服務 process.on("message", (data, server) => { http.createServer((req, res) => { res.end(`child${process.pid}`); }).listen(server); // 子進程共用主進程的服務 });
上面代碼中由主進程處理的請求會返回 hello,由子進程處理的請求會返回 child 加進程的 pid 組成的字符串。
2、使用 cluster 實現集群
cluster 模塊是 NodeJS 提供的用來實現集群的,他將 child_process 創建子進程的方法集成進去,實現方式要比使用 ipc 更簡潔。
// 文件:cluster.js const cluster = require("cluster"); const http = require("http"); const os = require("os"); // 判斷當前執行的進程是否為主進程,為主進程則創建子進程,否則用子進程監聽服務 if (cluster.isMaster) { // 創建子進程 os.cpus().forEach(() => cluster.fork()); } else { // 創建并監聽服務 http.createServer((req, res) => { res.end(`child${process.pid}`); }).listen(3000); }
上面代碼既會執行 if 又會執行 else,這看似很奇怪,但其實不是在同一次執行的,主進程執行時會通過 cluster.fork 創建子進程,當子進程被創建會將該文件再次執行,此時則會執行 else 中對服務的監聽,還有另一種用法將主進程和子進程執行的代碼拆分開,邏輯更清晰,用法如下。
// 文件:cluster.js const cluster = require("cluster"); const path = require("path"); const os = require("os"); // 設置子進程讀取文件的路徑 cluster.setupMaster({ exec: path.join(__dirname, "cluster-server.js") }); // 創建子進程 os.cpus().forEach(() => cluster.fork());
// 文件:cluster-server.js const http = require("http"); // 創建并監聽服務 http.createServer((req, res) => { res.end(`child${process.pid}`); }).listen(3000);
通過 cluster.setupMaster 設置子進程執行文件以后,就可以將主進程和子進程的邏輯拆分開,在實際的開發中這樣的方式也是最常用的,耦合度低,可讀性好,更符合開發的原則。
以上是“NodeJS中多進程和集群的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。