南京核酸最新通知重慶網(wǎng)站seo公司
Node.js 工作線程與子進(jìn)程:應(yīng)該使用哪一個
并行處理在計(jì)算密集型應(yīng)用程序中起著至關(guān)重要的作用。例如,考慮一個確定給定數(shù)字是否為素?cái)?shù)的應(yīng)用程序。如果我們熟悉素?cái)?shù),我們就會知道必須從 1 遍歷到該數(shù)的平方根才能確定它是否是素?cái)?shù),而這通常非常耗時且計(jì)算量極大。
因此,如果我們在 Node.js 上構(gòu)建此類計(jì)算量大的應(yīng)用程序,我們可能會阻塞正在運(yùn)行的線程很長時間。由于 Node.js 的單線程特性,不涉及 I/O 的計(jì)算密集型操作將導(dǎo)致應(yīng)用程序停止,直到該任務(wù)完成。
因此,在構(gòu)建需要執(zhí)行此類任務(wù)的軟件時,我們不會使用 Node.js。但是,Node.js 引入了工作線程 和子進(jìn)程的概念 來幫助在 Node.js 應(yīng)用程序中進(jìn)行并行處理,以便我們可以并行執(zhí)行特定進(jìn)程。在本文中,我們將了解這兩個概念并討論何時使用它們。
Node.js 中的工作線程是什么
Node.js 能夠有效地處理 I/O 操作。然而,當(dāng)它遇到任何計(jì)算量大的操作時,它會導(dǎo)致主事件循環(huán)凍結(jié)。
當(dāng) Node.js 發(fā)現(xiàn)異步操作時,它將其“離岸”到線程池。但是,當(dāng)它需要運(yùn)行計(jì)算量大的操作時,它會在其主線程上執(zhí)行該操作,這會導(dǎo)致應(yīng)用程序阻塞,直到操作完成。因此,為了緩解這個問題,Node.js 引入了工作線程的概念,以幫助從主事件循環(huán)中卸載 CPU 密集型操作,以便開發(fā)人員可以以非阻塞方式并行生成多個線程。
它通過啟動一個隔離的 Node.js 上下文來實(shí)現(xiàn)此目的,該上下文包含自己的 Node.js 運(yùn)行時、事件循環(huán)和事件隊(duì)列,該上下文在遠(yuǎn)程 V8 環(huán)境中運(yùn)行。這在與主事件循環(huán)斷開連接的環(huán)境中執(zhí)行,從而允許主事件循環(huán)釋放。
如上所示,Node.js 創(chuàng)建獨(dú)立的運(yùn)行時作為工作線程,其中每個線程獨(dú)立于其他線程執(zhí)行,并通過消息通道將其進(jìn)程狀態(tài)傳達(dá)給父線程。這允許父線程繼續(xù)照常執(zhí)行其功能(不會被阻塞)。通過這樣做,我們可以在 Node.js 中實(shí)現(xiàn)多線程。
在 Node.js 中使用工作線程有什么好處
正如我們所看到的,使用工作線程對于 CPU 密集型應(yīng)用程序非常有益。事實(shí)上,它有幾個優(yōu)點(diǎn):
- 性能改進(jìn):我們可以將計(jì)算繁重的操作轉(zhuǎn)移到工作線程,這可以釋放主線程,從而使我們的應(yīng)用程序能夠響應(yīng)更多請求。
- 提高并行性:如果我們有一個大型進(jìn)程,希望將其分成子任務(wù)并并行執(zhí)行,則可以使用工作線程來執(zhí)行此操作。例如,如果我們要確定 1,999,3241,123 是否為質(zhì)數(shù),則可以使用工作線程檢查范圍內(nèi)的除數(shù) -(WT1 中為 1 到 100,000,WT2 中為 100,001 到 200,000,等等)。這將加快我們的算法并導(dǎo)致更快的響應(yīng)。
什么時候應(yīng)該在 Node.js 中使用工作線程
如果我們考慮一下,我們應(yīng)該只使用工作線程來運(yùn)行與父線程隔離的計(jì)算密集型操作。
在工作線程中運(yùn)行 I/O 操作是沒有意義的,因?yàn)樗鼈円呀?jīng)被轉(zhuǎn)移到事件循環(huán)中。因此,當(dāng)我們需要在隔離環(huán)境中執(zhí)行計(jì)算量大的操作時,請考慮使用工作線程。
如何在 Node.js 中構(gòu)建工作線程
如果所有這些聽起來對我們很有吸引力,那么讓我們看看如何在 Node.js 中實(shí)現(xiàn)工作線程。考慮下面的代碼片段:
const {Worker,isMainThread,parentPort,workerData,
} = require("worker_threads");const { generatePrimes } = require("./prime");const threads = new Set();
const number = 999999;const breakIntoParts = (number, threadCount = 1) => {const parts = [];const chunkSize = Math.ceil(number / threadCount);for (let i = 0; i < number; i += chunkSize) {const end = Math.min(i + chunkSize, number);parts.push({ start: i, end });}return parts;
};if (isMainThread) {const parts = breakIntoParts(number, 5);parts.forEach((part) => {threads.add(new Worker(__filename, {workerData: {start: part.start,end: part.end,},}));});threads.forEach((thread) => {thread.on("error", (err) => {throw err;});thread.on("exit", () => {threads.delete(thread);console.log(`Thread exiting, ${threads.size} running...`);});thread.on("message", (msg) => {console.log(msg);});});
} else {const primes = generatePrimes(workerData.start, workerData.end);parentPort.postMessage(`Primes from - ${workerData.start} to ${workerData.end}: ${primes}`);
}
上面的代碼片段展示了一個可以利用工作線程的理想場景。要構(gòu)建工作線程,我們需要從庫中導(dǎo)入Worker、IsMainThread、parentPort和 workerDataworker_threads 。這些定義將用于創(chuàng)建工作線程。
上面的代碼創(chuàng)建了一種算法,可以查找給定范圍內(nèi)的所有素?cái)?shù)。它將主線程中的范圍分成不同的部分(上面示例中的五個部分),然后使用 new Worker() 來創(chuàng)建一個工作線程來處理每個部分。工作線程執(zhí)行 else塊,該塊在分配給該工作線程的范圍內(nèi)查找素?cái)?shù),并最終使用 parentPort.postMessage() 將結(jié)果發(fā)送回父(主)線程。
Node.js 中的子進(jìn)程是什么
子進(jìn)程與工作線程不同。雖然工作線程在同一進(jìn)程中提供隔離的事件循環(huán)和 V8 運(yùn)行時,但子進(jìn)程是整個 Node.js 運(yùn)行時的單獨(dú)實(shí)例。每個子進(jìn)程都有自己的內(nèi)存空間,并通過消息流或管道(或文件、數(shù)據(jù)庫、TCP/UDP 等)等 IPC(進(jìn)程間通信)技術(shù)與主進(jìn)程進(jìn)行通信。
在 Node.js 中使用子進(jìn)程有什么好處
在 Node.js 應(yīng)用程序中使用子進(jìn)程會帶來很多好處:
- 改進(jìn)的隔離性:每個子進(jìn)程都在自己的內(nèi)存空間中運(yùn)行,提供與主進(jìn)程的隔離。這對于可能存在資源沖突或需要分離的依賴性的任務(wù)是有利的。
- 提高可擴(kuò)展性:子進(jìn)程在多個進(jìn)程之間分配任務(wù),這使我們可以利用多核系統(tǒng)并處理更多并發(fā)請求。
- 提高魯棒性:如果子進(jìn)程由于某種原因崩潰,它不會隨之崩潰我們的主進(jìn)程。
- 運(yùn)行外部程序:子進(jìn)程允許我們將外部程序或腳本作為單獨(dú)的進(jìn)程運(yùn)行。這對于需要與其他可執(zhí)行文件交互的場景非常有用。
什么時候應(yīng)該在 Node.js 中使用子進(jìn)程
所以,現(xiàn)在我們知道子進(jìn)程給圖片帶來的好處了。了解何時應(yīng)該在 Node.js 中使用子進(jìn)程非常重要。根據(jù)我的經(jīng)驗(yàn),當(dāng)我們想在 Node.js 中執(zhí)行外部程序時,我建議我們使用子進(jìn)程。
比如存在一種使用場景:我們必須從 Node.js 服務(wù)中運(yùn)行外部可執(zhí)行文件。不可能在主線程內(nèi)執(zhí)行二進(jìn)制文件。因此,我們必須使用一個子進(jìn)程來執(zhí)行二進(jìn)制文件。
如何在 Node.js 中構(gòu)建子進(jìn)程
在 Node.js 中創(chuàng)建子進(jìn)程的方法有多種,可以使用 spawn() 、fork()、exec() 與 execFile() 等方法。
const { spawn } = require('child_process');
const child = spawn('node', ['child.js']);child.stdout.on('data', (data) => {console.log(`Child process stdout: ${data}`);
});child.on('close', (code) => {console.log(`Child process exited with code ${code}`);
});
我們所要做的就是從child_process模塊導(dǎo)入一個 spawn() 方法,然后通過傳遞 CLI 參數(shù)作為參數(shù)來調(diào)用該方法。
如何在工作線程和子進(jìn)程之間進(jìn)行選擇
現(xiàn)在我們知道什么是子進(jìn)程和工作線程,那么了解何時使用這些技術(shù)很重要。它們都不是適合所有情況的靈丹妙藥。這兩種方法都適用于特定條件。
在以下情況下使用工作線程:
- 我們正在運(yùn)行 CPU 密集型任務(wù)。如果我們的任務(wù)是 CPU 密集型的,那么工作線程是一個不錯的選擇。
- 我們的任務(wù)需要線程之間的共享內(nèi)存和高效通信。工作線程具有對共享內(nèi)存和用于通信的消息系統(tǒng)的內(nèi)置支持。
在以下情況下使用子進(jìn)程:
- 我們正在運(yùn)行需要隔離并獨(dú)立運(yùn)行的任務(wù),特別是當(dāng)它們涉及外部程序或腳本時。每個子進(jìn)程都在自己的內(nèi)存空間中運(yùn)行。
- 我們需要使用 IPC 機(jī)制(例如標(biāo)準(zhǔn)輸入/輸出流、消息傳遞或事件)在進(jìn)程之間進(jìn)行通信。子進(jìn)程非常適合此目的。
總結(jié)
并行處理正在成為現(xiàn)代系統(tǒng)設(shè)計(jì)的一個重要方面,特別是在構(gòu)建處理非常大的數(shù)據(jù)集或計(jì)算密集型任務(wù)的應(yīng)用程序時。因此,在使用 Node.js 構(gòu)建此類應(yīng)用程序時,考慮工作線程和子進(jìn)程非常重要。
如果我們的系統(tǒng)沒有采用正確的并行處理技術(shù)進(jìn)行正確設(shè)計(jì),我們的系統(tǒng)可能會因過度耗盡資源而表現(xiàn)不佳(因?yàn)樯蛇@些資源也會消耗大量資源)。
因此,對于軟件工程師和架構(gòu)師來說,清楚地驗(yàn)證需求并根據(jù)本文中提供的信息選擇正確的工具非常重要。