詳解如何在Node.js中執(zhí)行CPU密集型任務(wù)
本文是轉(zhuǎn)譯自一位國外大佬的文章,感覺對node.js任務(wù)執(zhí)行機制的解釋非常清楚透徹。
原文作者:Yarin Ronel
原文鏈接:Running CPU-Bound Tasks in Node.js: Introduction to Worker Threads
正文
Node.js通常被認為不適合CPU密集型應(yīng)用程序。Node.js的工作原理使其在處理I/O密集型任務(wù)時大放異彩。但相對的也導致其在處理CPU密集型任務(wù)中功虧一簣。話雖如此,雖然執(zhí)行CPU密集型任務(wù)肯定不是Node的主要使用場景,但是我們依舊有方法來改善這些問題。Node.js自誕生以來在這方面已經(jīng)在這些問題處理上取得了重大進展,現(xiàn)在我們應(yīng)該能夠做到基于合理的性能場景來執(zhí)行CPU密集型任務(wù)。
首先我們需要先思考以下三個問題:
- 1.什么是cpu密集型任務(wù)?
- 2.為什么 Node.js在執(zhí)行cpu密集型任務(wù)表現(xiàn)不佳?
- 3.如何克服這些問題,并且在 Node.js中有效運行cpu密集型任務(wù)?
1.什么是 CPU 密集型(I/O 密集型)任務(wù)?
首先,CPU 密集型任務(wù),專業(yè)術(shù)語是"CPU-bound";I/O 密集型任務(wù),專業(yè)術(shù)語是"I/O-bound"。大多數(shù)程序要么受 CPU 限制,要么受 I/O 限制,這意味著它們的執(zhí)行速度受 CPU 或 I/O(輸入/輸出)子系統(tǒng)(磁盤、網(wǎng)絡(luò)等)的限制。CPU密集型任務(wù)的示例包括需要大量計算或操作的任務(wù),例如:圖像處理、壓縮算法、矩陣乘法或是非常長(可能是嵌套)的循環(huán)。I/O 密集型任務(wù)的一些示例包括從磁盤中讀取文件、發(fā)出網(wǎng)絡(luò)請求或者查詢數(shù)據(jù)庫等。
Node.js 由于其架構(gòu)和執(zhí)行模型,在處理 I/O 密集型任務(wù)時表現(xiàn)出色,但在處理 CPU 密集型任務(wù)時往往效果不盡如人意。
2.為什么 Node.js在執(zhí)行cpu密集型任務(wù)表現(xiàn)不佳?
為了理解Node.js在執(zhí)行cpu密集型任務(wù)時遇到問題的原因,我們需要更深入的來討論它的內(nèi)部工作原理。根因在于 Node.js 的設(shè)計方式,通過查看其架構(gòu)和執(zhí)行模型,我們可以了解Node.js在這些特定情況下的執(zhí)行方式。
Node.js 的工作原理是怎樣的?
Node.js 本質(zhì)上是單線程的,它基于事件驅(qū)動架構(gòu),并提供 API 來訪問非阻塞 I/O。大多數(shù)其他編程語言在運行時會使用多線程來處理它們的并發(fā)性,而 Node.js 則是使用事件循環(huán)和非阻塞 I/O 來實現(xiàn)并發(fā)的。簡單來說,例如用 Java 編寫的 Web 服務(wù)器可能會為每個傳入請求(或為每個客戶端)分配一個單獨的線程,而 Node.js 是在單個線程中處理所有的傳入請求并使用事件循環(huán)來協(xié)調(diào)程序工作。
一個經(jīng)典的 Java 服務(wù)器可能如下圖所示,每個客戶端分配自己的線程,這意味著允許服務(wù)器去并行處理多個客戶端的請求。線程有時往往并未被充分利用,當特定客戶端沒有任務(wù)處理時,就會處于空閑狀態(tài):

而一個 Node.js 服務(wù)器則可能如下圖所示,它使用單個線程來處理來自所有不同客戶端的請求,這會導致更高的利用率(因為線程只有在沒有任何工作要執(zhí)行時才會處于空閑狀態(tài)):

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

