詳解如何在Node.js中執(zhí)行CPU密集型任務(wù)
本文是轉(zhuǎn)譯自一位國(guó)外大佬的文章,感覺(jué)對(duì)node.js任務(wù)執(zhí)行機(jī)制的解釋非常清楚透徹。
原文作者:Yarin Ronel
原文鏈接:Running CPU-Bound Tasks in Node.js: Introduction to Worker Threads
正文
Node.js通常被認(rèn)為不適合CPU密集型應(yīng)用程序。Node.js的工作原理使其在處理I/O密集型任務(wù)時(shí)大放異彩。但相對(duì)的也導(dǎo)致其在處理CPU密集型任務(wù)中功虧一簣。話雖如此,雖然執(zhí)行CPU密集型任務(wù)肯定不是Node的主要使用場(chǎng)景,但是我們依舊有方法來(lái)改善這些問(wèn)題。Node.js自誕生以來(lái)在這方面已經(jīng)在這些問(wèn)題處理上取得了重大進(jìn)展,現(xiàn)在我們應(yīng)該能夠做到基于合理的性能場(chǎng)景來(lái)執(zhí)行CPU密集型任務(wù)。
首先我們需要先思考以下三個(gè)問(wèn)題:
- 1.什么是cpu密集型任務(wù)?
- 2.為什么 Node.js在執(zhí)行cpu密集型任務(wù)表現(xiàn)不佳?
- 3.如何克服這些問(wèn)題,并且在 Node.js中有效運(yùn)行cpu密集型任務(wù)?
1.什么是 CPU 密集型(I/O 密集型)任務(wù)?
首先,CPU 密集型任務(wù),專(zhuān)業(yè)術(shù)語(yǔ)是"CPU-bound";I/O 密集型任務(wù),專(zhuān)業(yè)術(shù)語(yǔ)是"I/O-bound"。大多數(shù)程序要么受 CPU 限制,要么受 I/O 限制,這意味著它們的執(zhí)行速度受 CPU 或 I/O(輸入/輸出)子系統(tǒng)(磁盤(pán)、網(wǎng)絡(luò)等)的限制。CPU密集型任務(wù)
的示例包括需要大量計(jì)算或操作的任務(wù),例如:圖像處理、壓縮算法、矩陣乘法或是非常長(zhǎng)(可能是嵌套)的循環(huán)。I/O 密集型任務(wù)
的一些示例包括從磁盤(pán)中讀取文件、發(fā)出網(wǎng)絡(luò)請(qǐng)求或者查詢(xún)數(shù)據(jù)庫(kù)等。
Node.js 由于其架構(gòu)和執(zhí)行模型,在處理 I/O 密集型任務(wù)時(shí)表現(xiàn)出色,但在處理 CPU 密集型任務(wù)時(shí)往往效果不盡如人意。
2.為什么 Node.js在執(zhí)行cpu密集型任務(wù)表現(xiàn)不佳?
為了理解Node.js在執(zhí)行cpu密集型任務(wù)時(shí)遇到問(wèn)題的原因,我們需要更深入的來(lái)討論它的內(nèi)部工作原理。根因在于 Node.js 的設(shè)計(jì)方式,通過(guò)查看其架構(gòu)和執(zhí)行模型,我們可以了解Node.js在這些特定情況下的執(zhí)行方式。
Node.js 的工作原理是怎樣的?
Node.js 本質(zhì)上是單線程的,它基于事件驅(qū)動(dòng)架構(gòu),并提供 API 來(lái)訪問(wèn)非阻塞 I/O。大多數(shù)其他編程語(yǔ)言在運(yùn)行時(shí)會(huì)使用多線程來(lái)處理它們的并發(fā)性,而 Node.js 則是使用事件循環(huán)和非阻塞 I/O 來(lái)實(shí)現(xiàn)并發(fā)
的。簡(jiǎn)單來(lái)說(shuō),例如用 Java 編寫(xiě)的 Web 服務(wù)器可能會(huì)為每個(gè)傳入請(qǐng)求(或?yàn)槊總€(gè)客戶(hù)端)分配一個(gè)單獨(dú)的線程,而 Node.js 是在單個(gè)線程中處理所有的傳入請(qǐng)求并使用事件循環(huán)來(lái)協(xié)調(diào)程序工作。
一個(gè)經(jīng)典的 Java 服務(wù)器可能如下圖所示,每個(gè)客戶(hù)端分配自己的線程,這意味著允許服務(wù)器去并行處理多個(gè)客戶(hù)端的請(qǐng)求。線程有時(shí)往往并未被充分利用,當(dāng)特定客戶(hù)端沒(méi)有任務(wù)處理時(shí),就會(huì)處于空閑狀態(tài):
而一個(gè) Node.js 服務(wù)器則可能如下圖所示,它使用單個(gè)線程來(lái)處理來(lái)自所有不同客戶(hù)端的請(qǐng)求,這會(huì)導(dǎo)致更高的利用率(因?yàn)榫€程只有在沒(méi)有任何工作要執(zhí)行時(shí)才會(huì)處于空閑狀態(tài)):
在 Node.js 的單個(gè)實(shí)例中,由于只有一個(gè)線程在執(zhí)行 JavaScript,因此該工作實(shí)際上并非并行執(zhí)行的。
那么 Node.js 是如何在單線程的情況下實(shí)現(xiàn)足夠的性能的呢?它又是如何一次處理多個(gè)請(qǐng)求的呢?
這便是異步非阻塞 I/O 和事件循環(huán)
的作用: Node.js 以非阻塞的方式運(yùn)行 I/O 操作,這也就意味著它可以在 I/O 操作進(jìn)行時(shí)去執(zhí)行其他的代碼(甚至是其他的 I/O 操作),Node.js不必“等待” I/O 操作完成(甚至基本上會(huì)浪費(fèi)CPU周期閑置),而是可以利用這些時(shí)間來(lái)執(zhí)行其他任務(wù)。當(dāng) I/O 操作完成時(shí),事件循環(huán)負(fù)責(zé)將控制權(quán)交還給等待該 I/O 操作結(jié)果的代碼段。
簡(jiǎn)單來(lái)說(shuō),就是代碼的 CPU 綁定部分在單個(gè)線程中同步執(zhí)行,但 I/O 操作是異步執(zhí)行
的,不會(huì)阻塞主線程中的執(zhí)行(因此也被稱(chēng)為非阻塞)。事件循環(huán)負(fù)責(zé)協(xié)調(diào)工作,并決定在任何給定時(shí)間應(yīng)該執(zhí)行什么,本質(zhì)上,這就是 Node.js 實(shí)現(xiàn)并發(fā)
的方式!
如果 Node.js 是單線程的,那么它又怎么可能會(huì)有非阻塞 I/O 呢 ?當(dāng)I/O操作在當(dāng)前正在執(zhí)行中時(shí),如何去執(zhí)行其他代碼和其他I/O操作呢?
回答上述問(wèn)題之前,首先我們要明確一點(diǎn): Node.js 并不完全是單線程的
。Node.js 的確是單線程執(zhí)行模型,即只有單一的 JavaScript 指令可以在任何特定時(shí)間運(yùn)行在同一個(gè)范圍內(nèi)(這種單線程執(zhí)行模型并不是 JavaScript 語(yǔ)言本身的產(chǎn)物,事實(shí)上 JavaScript 作為一種語(yǔ)言既不是天生的單線程也不是多線程的,語(yǔ)言規(guī)范也不需要,最終決定取決于運(yùn)行時(shí)環(huán)境,例如:Node.js 或者瀏覽器)。但是這并不意味著 Node.js 不能利用額外的線程去運(yùn)行代碼甚至是產(chǎn)生額外的線程在單獨(dú)的上下文環(huán)境中運(yùn)行 JavaScript
。
首先,當(dāng)我們談到 Node.js 的底層的模型和原理時(shí),腦海中第一反應(yīng)想到的便是 JavaScript 的V8引擎,它是在 Node.js 中實(shí)際執(zhí)行 JavaScript 代碼的組件,V8 引擎基于 C++ 編寫(xiě)、以高度優(yōu)化著稱(chēng)的開(kāi)源引擎
,它由 Google 維護(hù)并被用作 Chrome 中的 JavaScript 引擎。
其次,Node.js 存在 libuv 的依賴(lài)項(xiàng),它是一個(gè)原生的開(kāi)源庫(kù),并為 Node.js 提供了異步、非阻塞的 I/O,還實(shí)現(xiàn)了 Node.js 的事件循環(huán)。最早由 Node.js 的作者開(kāi)發(fā),專(zhuān)門(mén)為 Node.js 提供多平臺(tái)下的異步 I/O 支持,其本身是由 C++ 語(yǔ)言實(shí)現(xiàn)的。在 Windows 環(huán)境下,libuv 直接使用 Windows 的 IOCP 來(lái)實(shí)現(xiàn)異步 I/O;在非 Windows 環(huán)境下,libuv 使用多線程來(lái)模擬異步 I/O。 Node.js 的異步調(diào)用是由 libuv 來(lái)支持
的,例如在讀取文件數(shù)據(jù)的過(guò)程中,讀取文件實(shí)質(zhì)的系統(tǒng)調(diào)用是由 libuv 來(lái)完成的,Node.js 只是負(fù)責(zé)調(diào)用 libuv 的接口,等待接口數(shù)據(jù)返回后再執(zhí)行對(duì)應(yīng)的回調(diào)方法。
目前 Node.js 運(yùn)行的各類(lèi)操作系統(tǒng)已經(jīng)能夠?yàn)榇蠖鄶?shù) I/O 操作提供了非阻塞機(jī)制,但是每個(gè)操作系統(tǒng)的實(shí)現(xiàn)方式卻各不相同,甚至即使在同一個(gè)操作系統(tǒng)中,有些 I/O 操作也與其他操作不同,有些甚至還沒(méi)有非阻塞機(jī)制。為了解決各類(lèi)操作系統(tǒng)的這些不一致性,libuv 提供了一個(gè)圍繞非阻塞 I/O 的抽象概念,它統(tǒng)一了跨不同操作系統(tǒng)和 I/O 操作的非阻塞行為,從而允許 Node.js 與所有主流操作系統(tǒng)兼容。libuv 還維護(hù)著一個(gè)內(nèi)部線程池,用于卸載在操作系統(tǒng)級(jí)別不存在非阻塞變體的 I/O 操作,另外,它還利用這個(gè)線程池來(lái)實(shí)現(xiàn)一些常見(jiàn)的 CPU 密集型操作的非阻塞任務(wù),例如:crypto(散列)和 zlib 模塊(壓縮)。
現(xiàn)在我們應(yīng)該大概能理解為什么在 Node.js 中運(yùn)行 CPU 密集型任務(wù)會(huì)性能表現(xiàn)不佳了。Node.js 使用單個(gè)線程來(lái)處理多個(gè)客戶(hù)端,雖然在I/O 操作上的確可以異步執(zhí)行,但在 CPU 密集型代碼方面卻不能
,這也意味著太多或太長(zhǎng)的 CPU 密集型任務(wù)可能會(huì)使得主線程忙于處理其他請(qǐng)求而導(dǎo)致阻塞。
Node.js 的執(zhí)行機(jī)制旨在滿(mǎn)足絕大多數(shù) Web 服務(wù)器的需求,這些服務(wù)器往往是 I/O 密集型的,為了達(dá)到這一目標(biāo),它犧牲了高效運(yùn)行 CPU 密集型代碼的能力。這也代表著,我們?yōu)榱四艹浞掷?Node.js 的能力,需要保證不要阻塞事件循環(huán),即確保每個(gè)回調(diào)方法都能快速完成以便最小化 CPU 密集型任務(wù)
。
3.如何克服這些問(wèn)題,并且在 Node.js中有效運(yùn)行cpu密集型任務(wù)?
既然我們知道為什么在Node.js中運(yùn)行CPU密集型任務(wù)會(huì)具有極大的挑戰(zhàn)性,那么讓我們來(lái)舉個(gè)例子來(lái)說(shuō)明。對(duì)于這個(gè)例子,我們選擇一個(gè)計(jì)算成本很高的例子來(lái)展示這個(gè)問(wèn)題,增加難度??梢宰屛覀兞私庠贜ode.js中運(yùn)行CPU密集型代碼所涉及的問(wèn)題。為了產(chǎn)生一個(gè)有足夠計(jì)算成本、可能需要足夠時(shí)長(zhǎng)的任務(wù)來(lái)阻塞事件循環(huán),我們可以選擇計(jì)算斐波那契數(shù)列
(其時(shí)間復(fù)雜度為O(2^N),隨著 N 值的增加,這個(gè)算法執(zhí)行起來(lái)會(huì)變得非常緩慢)。
現(xiàn)在,假設(shè)我們想要?jiǎng)?chuàng)建一個(gè)http服務(wù)器,該服務(wù)器將根據(jù)請(qǐng)求生成斐波那契數(shù)。與大多數(shù)其他web服務(wù)器一樣,我們的服務(wù)器應(yīng)該能夠?yàn)槎鄠€(gè)并發(fā)客戶(hù)端提供服務(wù)。除了生成斐波那契數(shù),我們的服務(wù)器還應(yīng)該用Hello World 作為結(jié)果來(lái)響應(yīng)任何其他請(qǐng)求。
首先,為我們的新服務(wù)器創(chuàng)建一個(gè)新目錄。在該目錄中,創(chuàng)建一個(gè)名為fibonacci.js的新文件。在該文件中,我們將實(shí)現(xiàn)我們的fibonacci函數(shù)并將其導(dǎo)出,以便其他模塊可以使用它。
// fibonacci.js function fibonacci(num) { if (num <= 1) return num; return fibonacci(num - 1) + fibonacci(num - 2); } module.exports = fibonacci;
然后再實(shí)現(xiàn)一個(gè)http服務(wù)器,這樣我們就可以在網(wǎng)絡(luò)上公開(kāi)我們的fibonacci功能了。為了實(shí)現(xiàn)我們的服務(wù)器,我們將使用Node的內(nèi)置http模塊,讓我們首先在一個(gè)名為index.js的新模塊中實(shí)現(xiàn)一個(gè)盡可能簡(jiǎn)單的服務(wù)器,并執(zhí)行node index.js
啟動(dòng)這個(gè)服務(wù)器:
const http = require('http'); const port = 3000; http .createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); console.log('Incoming request to:', url.pathname); res.writeHead(200); return res.end('Hello World!'); }) .listen(port, () => console.log(`Listening on port ${port}...`));
等服務(wù)器啟動(dòng)正常之后,我們可以簡(jiǎn)單在我們的瀏覽器中打開(kāi) http://localhost:3000/ ,或者使用curl等工具向我們的服務(wù)器發(fā)送GET請(qǐng)求。在終端窗口中執(zhí)行:
$ curl --get "http://localhost:3000/" Hello World!
我們的服務(wù)器正在工作并響應(yīng)傳入的請(qǐng)求?,F(xiàn)在讓我們讓它做我們真正希望它做的事情:生成斐波那契數(shù)!為此,我們將首先將從fibonacci.js導(dǎo)出的fibonacci函數(shù)導(dǎo)入到服務(wù)器模塊中。然后,我們將修改服務(wù)器,以便對(duì)路徑/fibonacci(格式為/fibonacci/?n=<Number>
)發(fā)出的傳入請(qǐng)求將提取所提供的參數(shù)(n),運(yùn)行導(dǎo)入的fibonacci函數(shù)以生成相關(guān)的fibonacci數(shù),并將其作為對(duì)該請(qǐng)求的響應(yīng)返回。最后,我們的index.js模塊應(yīng)該是這樣的(更改后的行高亮顯示):
const http = require('http'); const fibonacci = require('./fibonacci'); const port = 3000; http .createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); console.log('Incoming request to:', url.pathname); if (url.pathname === '/fibonacci') { const n = Number(url.searchParams.get('n')); console.log('Calculating fibonacci for', n); const result = fibonacci(n); res.writeHead(200); return res.end(`Result: ${result}`); } else { res.writeHead(200); return res.end('Hello World!'); } }) .listen(port, () => console.log(`Listening on port ${port}...`));
讓我們繼續(xù)測(cè)試我們的服務(wù)器新實(shí)現(xiàn)的功能:生成斐波那契數(shù)!您可能還記得,這些請(qǐng)求應(yīng)該是這樣的:/fibonacci/?n=<數(shù)字>
。讓我們用curl來(lái)試試:
$ curl --get "http://localhost:3000/fibonacci?n=13" Result: 233
不出所料,我們的服務(wù)器給出了第13個(gè)斐波那契數(shù),即233。服務(wù)器幾乎立即做出響應(yīng),這也是意料之中的事,因?yàn)?3是一個(gè)足夠小的輸入。如此之小,以至于即使是具有指數(shù)復(fù)雜性的函數(shù)(如我們的fibonacci函數(shù)實(shí)現(xiàn))也可以非??斓靥幚硭?。但是如果輸入更大的數(shù)字呢?
$ curl --get "http://localhost:3000/fibonacci?n=46" # About 21 seconds later... Result: 1836311903
執(zhí)行時(shí)間雖然取決于硬件,但我的筆記本電腦返回結(jié)果的時(shí)間不少于21.384秒。為了說(shuō)明增長(zhǎng)的速度有多快,計(jì)算第36個(gè)數(shù)字只花了202毫秒!這是一個(gè)巨大的區(qū)別。我們有意實(shí)現(xiàn)了一個(gè)效率不高的算法,所以執(zhí)行時(shí)間很長(zhǎng)是意料之中的事。當(dāng)我們的服務(wù)器必須處理多個(gè)并發(fā)請(qǐng)求時(shí),會(huì)發(fā)生什么呢
?
我們將從計(jì)算第46個(gè)斐波那契數(shù)的請(qǐng)求開(kāi)始,正如我們之前所看到的,這應(yīng)該需要大約21秒:
$ curl --get "http://localhost:3000/fibonacci?n=46"
當(dāng)?shù)谝粋€(gè)請(qǐng)求仍在運(yùn)行,沒(méi)有結(jié)束的時(shí)候,我們將從另一個(gè)終端發(fā)送另一個(gè)請(qǐng)求:
$ curl --get "http://localhost:3000/"
我們可以注意到第二個(gè)請(qǐng)求只是掛起,只有在第一個(gè)請(qǐng)求完成后才會(huì)處理。我們不得不等待21秒才能完成第二個(gè)請(qǐng)求,盡管它所要做的只是回復(fù)Hello World 這個(gè)簡(jiǎn)單的字符。我們前面章節(jié)中的假設(shè)得到了驗(yàn)證:在Node.js中運(yùn)行CPU密集型任務(wù),會(huì)導(dǎo)致阻塞的事件循環(huán)。這意味著所有進(jìn)一步的操作都被阻止,直到阻止操作結(jié)束。在我們的例子中,服務(wù)器忙于計(jì)算第46個(gè)斐波那契數(shù),甚至無(wú)法響應(yīng)Hello World!。運(yùn)行CPU密集型任務(wù)的困難是Node.js最大的缺點(diǎn)之一
。但是,正如我們前面提到的,有一些可能的解決方案。
4.可以借鑒的解決方案
既然我們了解了Node.js在運(yùn)行CPU密集型任務(wù)遇到問(wèn)題的原因,并且我們已經(jīng)了解了它在現(xiàn)實(shí)場(chǎng)景中的表現(xiàn),那么讓我們來(lái)談?wù)劷鉀Q方案。
- 使用 setImmediate()(單線程)拆分任務(wù)
- 開(kāi)啟新的子進(jìn)程
- 使用工作線程
4.1 拆分任務(wù) setImmediate()
與其他方法不同,這種方法不使用額外的CPU內(nèi)核。
了解了 Node.js 運(yùn)行 CPU 密集型任務(wù)時(shí)表現(xiàn)不佳的本質(zhì)原因,我們可以很清楚地看到它們都是源于一個(gè)基本事實(shí): 這些長(zhǎng)時(shí)間運(yùn)行的任務(wù)會(huì)阻塞事件循環(huán),使其保持忙碌狀態(tài)以至于它會(huì)完全停滯其他的所有任務(wù),甚至?xí)芙^執(zhí)行一個(gè)循環(huán)
。阻塞事件循環(huán)意味著其他任務(wù)完全停滯而不僅僅只是減速。但是,我們可以將這些長(zhǎng)時(shí)間運(yùn)行的的任務(wù)“拆分”為更小的、耗時(shí)更少的部分。這樣基本上是可以解決我們的問(wèn)題,雖然在吞吐量上不會(huì)增加,但是拆分長(zhǎng)時(shí)間運(yùn)行的任務(wù)可以顯著提高服務(wù)器的響應(yīng)能力,不會(huì)再有“輕量級(jí)”任務(wù)被一些長(zhǎng)時(shí)間運(yùn)行的、受 CPU 限制的“龐然大物”無(wú)限期地阻塞。
要想實(shí)現(xiàn)這樣的“拆分”效果,我們可以使用內(nèi)置的 setImmediate() 函數(shù),通過(guò)將我們的 CPU 綁定操作劃分為好幾個(gè)步驟,并用setImmediate() 來(lái)調(diào)用每個(gè)步驟,這樣我們基本上可以使事件循環(huán)在每個(gè)步驟之間進(jìn)行控制,事件循環(huán)將會(huì)在繼續(xù)下一步操作之前處理掛起的任務(wù)。這是在 Node.js 中處理 CPU 密集型任務(wù)的最基本的方法,但利弊相依,它也有著一些明顯的缺點(diǎn):
- 效率低下。延遲的每個(gè)步驟都會(huì)引入一些開(kāi)銷(xiāo),隨著步驟數(shù)量的增加,這種開(kāi)銷(xiāo)會(huì)變得越來(lái)越大;
- 僅當(dāng)一次運(yùn)行單個(gè) CPU 密集型任務(wù)時(shí)才會(huì)有用。這種方法確實(shí)可以幫助保持服務(wù)器的響應(yīng),但是它顯然不會(huì)使同時(shí)運(yùn)行的多個(gè) CPU 密集型任務(wù)運(yùn)行得更快,因?yàn)槲覀內(nèi)匀皇窃谑褂脝尉€程;
- 操作麻煩。將算法拆分成步驟有時(shí)很難顧全大局,可能會(huì)降低代碼的可讀性。
這種方法只有在是針對(duì)偶爾運(yùn)行一次的 CPU 密集型任務(wù)時(shí)才是最簡(jiǎn)單、最有效的方法
。
4.2 開(kāi)啟新的子進(jìn)程
我們?nèi)绻胍耆苊庠谥鲬?yīng)用程序中運(yùn)行昂貴的CPU綁定任務(wù),可以開(kāi)啟另一個(gè)單獨(dú)的進(jìn)程來(lái)運(yùn)行這些任務(wù)。通過(guò)這種方式,可以讓我們的主應(yīng)用程序做Node.js最擅長(zhǎng)的事情。我們遵守Node的規(guī)則,避免阻塞事件循環(huán)。作為對(duì)Node.js的正確的使用方式的回報(bào),我們的主要應(yīng)用程序?qū)⒃谛阅芊矫娅@得很大的提升。除此之外,在新的進(jìn)程中運(yùn)行的任務(wù)也會(huì)更快。
Node.js 提供了專(zhuān)門(mén)的 child_process 模塊來(lái)支持創(chuàng)建子進(jìn)程、管理它們并與它們通信。每個(gè)衍生的 Node.js 子進(jìn)程都是獨(dú)立的,并且擁有自己的內(nèi)存、事件循環(huán)和 V8 實(shí)例,子進(jìn)程和父進(jìn)程之間的唯一連接是兩者之間建立的通信通道。
首先,新創(chuàng)建一個(gè)名為fibonacci-fork.js的新模塊。這是我們的子進(jìn)程將運(yùn)行的模塊:
function fibonacci(num) { if (num <= 1) return num; return fibonacci(num - 1) + fibonacci(num - 2); } process.on('message', (message) => { process.send(fibonacci(message)); process.exit(0); });
然后,我們將修改我們之前的的服務(wù)器主程序(index.js)。它應(yīng)該處理如下事情:
- 基于我們新創(chuàng)建的模塊(fibonacci-fork.js),使用fork()創(chuàng)建一個(gè)子進(jìn)程
- 使用childProcess.send() 向子進(jìn)程發(fā)送一條包含所需參數(shù)(n)的消息
- 使用childProcess.on('message', () => {}) 監(jiān)聽(tīng)來(lái)自子進(jìn)程的消息
- 使用從子進(jìn)程接收到的結(jié)果并且響應(yīng)請(qǐng)求
// index.js const http = require('http'); const path = require('path'); const { fork } = require('child_process'); const port = 3000; http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); console.log('Incoming request to:', url.pathname); if (url.pathname === '/fibonacci') { const n = Number(url.searchParams.get('n')); console.log('Calculating fibonacci for', n); // 使用fork()基于fibonacci創(chuàng)建新子進(jìn)程 const childProcess = fork(path.join(__dirname, 'fibonacci')); // 接收從子進(jìn)程收到的結(jié)果響應(yīng)請(qǐng)求 childProcess.on('message', (message) => { res.writeHead(200); return res.end(`Result: ${message}`); }); // 向子進(jìn)程發(fā)送消息 childProcess.send(n); } else { res.writeHead(200); return res.end('Hello World!'); } }).listen(port, () => console.log(`Listening on port ${port}...`));
啟動(dòng)服務(wù)器之后,首先在終端中執(zhí)行:
$ curl --get "http://localhost:3000/fibonacci?n=46"
當(dāng)?shù)谝粋€(gè)請(qǐng)求仍在運(yùn)行,沒(méi)有結(jié)束的時(shí)候,我們將從另一個(gè)終端發(fā)送另一個(gè)請(qǐng)求:
$ curl --get "http://localhost:3000/"
即使當(dāng)前正在進(jìn)行/fibonacci請(qǐng)求,我們也能立刻拿到第二個(gè)請(qǐng)求的返回 hello world! 我們的JavaScript代碼首次可以利用多個(gè)CPU,實(shí)現(xiàn)了并發(fā)處理多任務(wù)的功能。
4.3 使用工作線程
工作線程 API 位于 worker_threads 模塊當(dāng)中,允許使用線程并行執(zhí)行 JavaScript。工作線程與子進(jìn)程非常相似,兩者都是通過(guò)將執(zhí)行任務(wù)委托給單獨(dú)的 Node.js 實(shí)例,可用于在主應(yīng)用程序的事件循環(huán)之外運(yùn)行 CPU 密集型任務(wù)。當(dāng)然,兩者之間還是有一個(gè)根本的區(qū)別,即子進(jìn)程是在完全不同的進(jìn)程中執(zhí)行工作,但工作線程卻是在主應(yīng)用程序的同一個(gè)進(jìn)程內(nèi)執(zhí)行工作
。
這個(gè)關(guān)鍵性的差異導(dǎo)致工作線程對(duì)于子進(jìn)程而言有一些非常好的優(yōu)點(diǎn):
與子進(jìn)程相比,工作線程是更輕量級(jí)的
。它可以消耗更少的內(nèi)存并且啟動(dòng)速度更快,但由于創(chuàng)建工作線程是十分昂貴的,所以它們通常應(yīng)該被慎重使用工作線程之間可以共享內(nèi)存
。除了我們?cè)谧舆M(jìn)程中看到的基本的消息傳遞之外,工作線程還可以傳輸 ArrayBuffer 實(shí)例,甚至共享SharedArrayBuffer 實(shí)例,這兩個(gè)對(duì)象都用于保存原始的二進(jìn)制數(shù)據(jù)(ArrayBuffer 和SharedArrayBuffer 之間的區(qū)別可以歸結(jié)為傳輸和共享之間的區(qū)別。一旦 ArrayBuffer 被轉(zhuǎn)移到一個(gè)工作線程,原來(lái)的 ArrayBuffer 就會(huì)被清除,并且不再由主線程或者其他任何工作線程訪問(wèn)或操作。SharedArrayBuffer 的內(nèi)容實(shí)際上是共享的,并且可以由主線程和任意數(shù)量的工作線程同時(shí)訪問(wèn)和操作,在這種情況下,同步則必須由開(kāi)發(fā)人員手動(dòng)來(lái)管理)。
Node.js 中的工作線程和其他經(jīng)典的線程編程語(yǔ)言(如:Java 或 C++)中的“經(jīng)典”線程完全不同。在 Node.js 中,每個(gè)工作線程都有屬于自己的獨(dú)立的 Node.js 運(yùn)行實(shí)例 (包括自己的 V8 實(shí)例、事件循環(huán)等)和自己的隔離上下文
。而后者往往是在相同的運(yùn)行時(shí)中運(yùn)行并處理主線程的相同上下文。
在線程編程語(yǔ)言中,默認(rèn)情況下任何線程都可以隨時(shí)訪問(wèn)和操作任何變量,即使有不同的線程當(dāng)前正在使用該變量。而之所以會(huì)發(fā)生這種情況,是因?yàn)樗械木€程都在同一上下文中運(yùn)行,這可能會(huì)導(dǎo)致一些嚴(yán)重的錯(cuò)誤。作為開(kāi)發(fā)人員,我們需要使用語(yǔ)言的同步機(jī)制來(lái)確保不同的線程之間協(xié)同工作。
而工作線程是在它自己的隔離上下文中運(yùn)行的,默認(rèn)情況下(除去 SharedArrayBuffer)它不與主線程或者其他任何工作線程共享任何內(nèi)容,因此我們通常不需要考慮線程同步。
兩者之間的這種差異既有消極的影響,也有積極的影響。一方面,工作線程可能會(huì)被認(rèn)為不如“經(jīng)典”線程強(qiáng)大,因?yàn)樗粔蜢`活;但另一方面,工作線程又比“經(jīng)典”線程安全得多得多。一般來(lái)說(shuō),“經(jīng)典”的多線程編程被認(rèn)為是困難且容易出錯(cuò)的(有些錯(cuò)誤很難被檢測(cè)出來(lái)),工作線程由于其在單獨(dú)的上下文中運(yùn)行并且在默認(rèn)情況下不共享任何內(nèi)容,因此基本上已經(jīng)消除了一整類(lèi)潛在的與多線程相關(guān)的錯(cuò)誤,例如:競(jìng)爭(zhēng)條件和死鎖,同時(shí)對(duì)絕大多數(shù)的用例保持足夠的靈活性。
worker_threads 模塊中的一些核心術(shù)語(yǔ)和方法(來(lái)源于官方文檔):
- new Worker(filename, [options])。用于創(chuàng)建新工作線程的構(gòu)造函數(shù),創(chuàng)建的工作線程將執(zhí)行由 filename 引用的模塊的內(nèi)容,options 可選,常見(jiàn)的選擇是 workerData;
- worker.workerData。可以是任何 JavaScript 值,這是使用該 workerData 選項(xiàng)傳遞給此工作線程構(gòu)造函數(shù)的數(shù)據(jù)的克隆,本質(zhì)上是一種在創(chuàng)建時(shí)為新的工作線程提供一些數(shù)據(jù)的方法;
- worker.isMainThread。如果代碼不在工作線程中運(yùn)行而是在主線程中運(yùn)行,則為真;
- worker.parentPort。工作線程和父線程之間雙向通信通道的工作端,從工作線程發(fā)送的消息parentPort.postMessage() 可以在主線程中使用 worker.on(‘message’),反之從主線程發(fā)送的消息 worker.postMessage() 可以在工作線程中使用 parentPort.on(‘message’)。
我們通過(guò)如下例子來(lái)實(shí)現(xiàn)。首先,我們必須創(chuàng)建一個(gè)新的模塊。我們稱(chēng)之為fibonacci-worker.js。這個(gè)模塊將包含最終將在工作線程中執(zhí)行的代碼。
const { Worker, isMainThread, parentPort, workerData, } = require('worker_threads'); function fibonacci(num) { if (num <= 1) return num; return fibonacci(num - 1) + fibonacci(num - 2); } if (isMainThread) { module.exports = (n) => new Promise((resolve, reject) => { const worker = new Worker(__filename, { workerData: n, }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code $[code]`)); } }); }); } else { const result = fibonacci(workerData); parentPort.postMessage(result); process.exit(0); }
我們已經(jīng)實(shí)現(xiàn)了worker模塊,剩下要做的就是更新服務(wù)器模塊(index.js):
const http = require('http'); const fibonacciWorker = require('./fibonacci-worker'); const port = 3000; http .createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); console.log('Incoming request to:', url.pathname); if (url.pathname === '/fibonacci') { const n = Number(url.searchParams.get('n')); console.log('Calculating fibonacci for', n); const result = await fibonacciWorker(n); res.writeHead(200); return res.end(`Result: ${result}\n`); } else { res.writeHead(200); return res.end('Hello World!'); } }) .listen(port, () => console.log(`Listening on port ${port}...`));
請(qǐng)注意,此處使用async/await,因此使用await調(diào)用了fibonacciWorker()(第15行),并聲明了服務(wù)器的回調(diào)函數(shù)async(第7行)。當(dāng)然,也可以使用傳統(tǒng)的.then(…)語(yǔ)法。
啟動(dòng)服務(wù)器之后,首先在終端中執(zhí)行:
$ curl --get "http://localhost:3000/fibonacci?n=46"
當(dāng)?shù)谝粋€(gè)請(qǐng)求仍在運(yùn)行,沒(méi)有結(jié)束的時(shí)候,我們將從另一個(gè)終端發(fā)送另一個(gè)請(qǐng)求:
$ curl --get "http://localhost:3000/"
正如預(yù)期的那樣,服務(wù)器保持響應(yīng)并立即響應(yīng)第二個(gè)請(qǐng)求,盡管當(dāng)前有一個(gè)CPU密集型任務(wù)正在運(yùn)行(由第一個(gè)請(qǐng)求啟動(dòng))。我們還可以發(fā)送多個(gè)/fibonacci請(qǐng)求,并讓它們并行執(zhí)行。
4.4 使用工作池
在上述子進(jìn)程
和工作線程
的示例代碼中,我們給每個(gè)傳入的請(qǐng)求創(chuàng)建了一個(gè)新的進(jìn)程/工作線程并且只使用了一次,這其實(shí)并不合適,主要因?yàn)椋?/p>
- 創(chuàng)建一個(gè)新的子進(jìn)程/工作線程是很昂貴的,為了獲得最佳的性能,我們應(yīng)該對(duì)其慎重使用;
- 我們無(wú)法控制創(chuàng)建的子進(jìn)程/工作線程的數(shù)量,這會(huì)使我們?nèi)菀资艿?DOS 攻擊。
我們可以使用工作池來(lái)管理我們的子進(jìn)程/工作線程,例如:workerpool 它適用于子進(jìn)程和工作線程。
總結(jié)
Node.js需要通過(guò)最大限度地減少運(yùn)行cpu密集型任務(wù)以提升服務(wù)器性能,雖然目前的狀態(tài)對(duì)許多應(yīng)用程序來(lái)說(shuō)已經(jīng)足夠了,但Node.js可能永遠(yuǎn)不會(huì)完全適合真正的CPU密集型應(yīng)用程序。其實(shí)并沒(méi)有沒(méi)關(guān)系,因?yàn)檫@不是Node的設(shè)計(jì)初衷。我們知道沒(méi)有一個(gè)軟件項(xiàng)目適用于所有情況。成功的軟件項(xiàng)目會(huì)進(jìn)行權(quán)衡,使他們能夠在核心用例中脫穎而出,同時(shí)(希望)留下一些靈活性的空間。
其實(shí)這也是Node.js正在做的事情,它在其處理I/O密集型任務(wù)方面非常出色。雖然它不擅長(zhǎng)處理CPU密集型應(yīng)用程序,但它仍然為開(kāi)發(fā)者提供了靈活、合理的方法來(lái)改善這一點(diǎn)。
以上就是詳解如何在Node.js中執(zhí)行CPU密集型任務(wù)的詳細(xì)內(nèi)容,更多關(guān)于在Node.js中執(zhí)行CPU任務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Node.js 使用axios讀寫(xiě)influxDB的方法示例
這篇文章主要介紹了Node.js 使用axios讀寫(xiě)influxDB的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10Node.js中npx命令的使用方法及場(chǎng)景分析
NPM(Node Package Manager) 是Node.js提供的一個(gè)包管理器, 可以使用 NPM 來(lái)安裝 node.js 包 ,npm 是從5.2版開(kāi)始, 增加(自帶)了 npx 命令,本文給大家分享Node.js npx命令使用,需要的朋友一起看看吧2021-08-08利用node實(shí)現(xiàn)數(shù)據(jù)庫(kù)數(shù)據(jù)導(dǎo)出到Excel
本文將詳細(xì)講解如何使用Node.js實(shí)現(xiàn)從MySQL數(shù)據(jù)庫(kù)獲取數(shù)據(jù),并生成包含多個(gè)工作表的 Excel 文件,每個(gè)工作表對(duì)應(yīng)數(shù)據(jù)庫(kù)中的一個(gè)表,有需要的可以了解下2024-11-11node.js連接MongoDB數(shù)據(jù)庫(kù)的2種方法教程
這幾天一直在學(xué)習(xí)mongdb的基礎(chǔ)知識(shí),跟著網(wǎng)上大神的腳步(代碼)去模擬連接mongodb數(shù)據(jù)庫(kù),下面這篇文章就給大家總結(jié)介紹了node.js連接MongoDB數(shù)據(jù)庫(kù)的2種方法教程,文中介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-05-05node.js中的events.EventEmitter.listenerCount方法使用說(shuō)明
這篇文章主要介紹了node.js中的events.EventEmitter.listenerCount方法使用說(shuō)明,本文介紹了events.EventEmitter.listenerCount的方法說(shuō)明、語(yǔ)法、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12基于Node的Axure文件在線預(yù)覽的實(shí)現(xiàn)代碼
這篇文章主要介紹了基于Node的Axure文件在線預(yù)覽的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08手把手教你實(shí)現(xiàn) Promise的使用方法
這篇文章主要介紹了手把手教你實(shí)現(xiàn) Promise的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Vue+Node服務(wù)器查詢(xún)Mongo數(shù)據(jù)庫(kù)及頁(yè)面數(shù)據(jù)傳遞操作實(shí)例分析
這篇文章主要介紹了Vue+Node服務(wù)器查詢(xún)Mongo數(shù)據(jù)庫(kù)及頁(yè)面數(shù)據(jù)傳遞操作,結(jié)合實(shí)例形式分析了node.js查詢(xún)MongoDB數(shù)據(jù)庫(kù)及vue前臺(tái)頁(yè)面渲染等相關(guān)操作技巧,需要的朋友可以參考下2019-12-12