Node處理CPU密集型任務的方法有哪些?以下這篇文章就來帶大家了解一下Node處理CPU密集型任務的方法,希望對大家有幫助!
我們在日常工作中或多或少聽過以下的話:
Node是一個
非阻塞I/O
(non-blocking I/O)和事件驅動
(event-driven)的JavaScript運行環境
(runtime),所以它非常適合用來建構I/O密集型應用,例如Web服務等。
不知道當你聽到類似的話時會不會有跟我一樣的疑惑:單執行緒的Node為什麼適合用來開發I/O密集型應用?
按道理來說不是那些支援多執行緒的語言(例如Java和Golang)做這些工作更有優勢嗎?
要搞清楚上面的問題,我們要知道Node的單執行緒指的是什麼。 【相關教學推薦:nodejs影片教學】
Node不是單執行緒的
其實我們說Node是單線程的,說的只是我們的JavaScript程式碼
是在同一個執行緒(我們可以叫它主執行緒
)裡面運行的,而不是說Node只有一個執行緒在工作
。實際上Node底層會使用libuv的多執行緒能力
將一部分工作(基本上都是I/O相關操作)放在一些主執行緒之外
的執行緒裡面執行,當這些任務完成後再以回呼函數
的方式將結果傳回主執行緒的JavaScript執行環境。可以看看示意圖:
註: 上圖是Node事件循環
(Event Loop)的簡化版,實際上完整的事件循環會有更多的階段例如timers等。
Node適合做I/O密集型應用
從上面的分析我們知道Node會將所有的I/O操作透過libuv的多執行緒能力分散到不同的執行緒裡面執行,其餘的操作都放在主執行緒裡面執行。那為什麼這種做法就比Java或Golang等其它語言更適合做I/O密集型應用呢?我們以開發Web服務為例,Java和Golang等主流後端程式語言的並發模型是基於線程
(Thread-Based)的,這也意味著他們對於每一個網路請求都會創建一個單獨的執行緒
來處理。可是對於Web應用來說,主要還是對資料庫的增刪改查,或者請求其它外部服務等網路I/O操作
,而這些操作最後都是交給作業系統的系統調用來處理的(無需應用線程參與),並且十分緩慢(相對於CPU時鐘週期來說)
,因此被創建出來的線程大多數時間是無事可做
的而且我們的服務還要承擔額外的執行緒切換
開銷。和這些語言不一樣的是Node沒有為每個請求都創建一個線程,所有請求的處理
都發生在主線程中,因此沒有了線程切換
的開銷,並且它也會透過執行緒池
的形式非同步處理這些I/O
操作,然後透過事件的形式告訴主執行緒結果從而避免阻塞主執行緒的執行,因此它理論上
是更有效率的。這裡值得注意的是我只是說Node理論上
是比較快的,其實真不一定。這是因為現實中一個服務的性能會受到很多方面的影響,我們這裡只是考慮了並發模型
這一個因素,而其它因素例如運行時消耗也會影響到服務的性能,舉個例子,JavaScript
是動態語言,資料的類型需要在執行時間推斷,而Golang
和Java
都是靜態語言它們的資料型別在編譯時就可以確定,所以它們實際執行起來可能會更快,佔用記憶體也會更少。
Node不適合做CPU密集型任務
上面我們提到Node除了I/O相關的作業其餘操作都會在主執行緒裡面執行,所以當Node要處理一些CPU密集型
的任務時,主執行緒會被阻塞住。讓我們來看一個CPU密集型任務的範例:
// node/cpu_intensive.js const http = require('http') const url = require('url') const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { hardWork() resp.write('hard work') resp.end() } else if (urlParsed.pathname === '/easy_work') { resp.write('easy work') resp.end() } else { resp.end() } }) server.listen(8080, () => { console.log('server is up...') })
在上面的程式碼中我們實作了擁有兩個介面的HTTP服務:/hard_work
介面是一個CPU密集型介面
,因為它呼叫了hardWork
這個CPU密集型
函數,而/easy_work
這個介面則很簡單,直接回傳一個字串給客戶端就可以了。為什麼說hardWork
函數是CPU密集型
的呢?這是因為它都是在CPU的運算子
裡面對i
進行算術運算而沒有進行任何I/O操作。啟動完我們的Node服務後,我們試著呼叫一下/hard_word
介面:
我們可以看到/hard_work
介面是會卡住的,這是因為它需要進行大量的CPU
計算,所以需要比較久的時間才會執行完。而這個時候我們再看一下/easy_work
這個介面有沒有影響:
我們發現在/hard_work
佔用了CPU資源之後,無辜的/easy_work
介面也被卡死了。原因就是hardWork
函數阻塞了Node的主執行緒導致/easy_work
的邏輯不會被執行。這裡值得一提的是,只有Node這種基於事件循環的單執行緒執行環境才會有這種問題,Java和Golang等Thread-Based語言是不會有這種問題的。那如果我們的服務真的需要執行CPU密集型
任務怎麼辦?總不能換門語言吧?說好的All in JavaScript
呢?別急,對於處理CPU密集型任務
,Node已經為我們準備好很多方案了,接下來就讓我為大家介紹三種常用的方案,它們分別是: Cluster Module
,Child Process
和Worker Thread
。
Cluster Module
概念介紹
Node很早(v0.8版本)就推出了Cluster模組。這個模組的作用就是透過一個父進程啟動一群子程序來對網路請求進行負載平衡
。因為文章的篇幅限制我們不會細聊Cluster模組有哪些API,有興趣的讀者後面可以看看官方文檔,這裡我們直接看一下如何使用Cluster模組來優化上面CPU密集型的場景:
// node/cluster.js const cluster = require('cluster') const http = require('http') const url = require('url') // 获取CPU核数 const numCPUs = require('os').cpus().length const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { console.log(`worker ${worker.process.pid} is online`) }) cluster.on('exit', (worker, code, signal) => { // 某个工作进程挂了之后,我们需要立马启动另外一个工作进程来替代 console.log(`worker ${worker.process.pid} exited with code ${code}, and signal ${signal}, start a new one...`) cluster.fork() }) } else { // 工作进程启动一个HTTP服务器 const server = http.createServer((req, resp) => { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { hardWork() resp.write('hard work') resp.end() } else if (urlParsed.pathname === '/easy_work') { resp.write('easy work') resp.end() } else { resp.end() } }) // 所有的工作进程都监听在同一个端口 server.listen(8080, () => { console.log(`worker ${process.pid} server is up...`) }) }
在上面的程式碼中我們根據目前裝置的CPU核數使用cluster.fork
函數創建了同等數量的工作進程
,而且這些工作進程都是監聽在8080
連接埠上面的。看到這裡你或許會問所有的進程都監聽在同一個端口會不會出現問題,這裡其實是不會的,因為Cluster
模組底層會做一些工作讓最終監聽在 8080
埠的是主程序
,而主行程是所有流量的入口
,它會接收HTTP連線並把它們打到不同的工作行程上面。話不多說,讓我們執行一下這個node服務:
從上面的輸出結果來看,cluster啟動了10個worker(我的電腦是10核心的)來處理web請求,這個時候我們再來請求一下/hard_work
這個介面:
#我們發現這個請求還是卡死的,接著我們再來看看Cluster模組有沒有解決其它請求也被阻塞
的問題:
#我們可以看到前面9個請求
都是很順利就回傳結果的,可是到了第10個請求
我們的介面就卡住了,這是為什麼呢?原因就是我們一共開了10個工作進程,主進程在將流量打到子進程的時候採用的預設負載平衡策略是round-robin
(輪流),因此第10個請求(其實是第11個,因為包括了第一個hard_work的請求)剛好回到第一個worker,而這個worker還沒處理完hard_work
的任務,因此這個easy_work
的任務也就卡住了。 cluster的負載平衡演算法可以透過cluster.schedulingPolicy
來修改,有興趣的讀者可以看一下官方文件。
从上面的结果来看Cluster Module似乎解决了一部分
我们的问题,可是还是有一些请求受到了影响。那么Cluster Module在实际开发里面能不能被用来解决这个CPU密集型
任务的问题呢?我的意见是:看情况。如果你的CPU密集型接口调用不频繁
而且运算时间不会太长
,你完全可以使用这种Cluster Module来优化。可是如果你的接口调用频繁并且每个接口都很耗时间的话,可能你需要看一下采用Child Process
或者Worker Thread
的方案了。
Cluster Module的优缺点
最后我们总结一下Cluster Module有什么优点:
-
资源利用率高
:可以充分利用CPU的多核能力
来提升请求处理效率。 -
API设计简单
:可以让你实现简单的负载均衡
和一定程度的高可用
。这里值得注意的是我说的是一定程度的高可用,这是因为Cluster Module的高可用是单机版的
,也就是当宿主机器挂了,你的服务也就挂了,因此更高的高可用肯定是使用分布式集群做的。 -
进程之间高度独立
,避免某个进程发生系统错误导致整个服务不可用。
优点说完了,我们再来说一下Cluster Module不好的地方:
-
资源消耗大
:每一个子进程都是独立的Node运行环境
,也可以理解为一个独立的Node程序,因此占用的资源也是巨大的
。 -
进程通信开销大
:子进程之间的通信通过跨进程通信(IPC)
来进行,如果数据共享频繁是一笔比较大的开销。 -
没能完全解决CPU密集任务
:处理CPU密集型任务时还是有点抓紧见肘
。
Child Process
在Cluster Module中我们可以通过启动更多的子进程来将一些CPU密集型的任务负载均衡到不同的进程里面,从而避免其余接口卡死。可是你也看到了,这个办法治标不治本
,如果用户频繁调用CPU密集型的接口,那么还是会有一大部分请求会被卡死的。优化这个场景的另外一个方法就是child_process
模块。
概念介绍
Child Process
可以让我们启动子进程
来完成一些CPU密集型任务。我们先来看一下主进程master_process.js
的代码:
// node/master_process.js const { fork } = require('child_process') const http = require('http') const url = require('url') const server = http.createServer((req, resp) => { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { // 对于hard_work请求我们启动一个子进程来处理 const child = fork('./child_process') // 告诉子进程开始工作 child.send('START') // 接收子进程返回的数据,并且返回给客户端 child.on('message', () => { resp.write('hard work') resp.end() }) } else if (urlParsed.pathname === '/easy_work') { // 简单工作都在主进程进行 resp.write('easy work') resp.end() } else { resp.end() } }) server.listen(8080, () => { console.log('server is up...') })
在上面的代码中对于/hard_work
接口的请求,我们会通过fork
函数开启一个新的子进程
来处理,当子进程处理完毕我们拿到数据后就给客户端返回结果。这里值得注意的是当子进程完成任务后我没有释放子进程的资源,在实际项目里面我们也不应该频繁创建和销毁子进程因为这个消耗也是很大的,更好的做法是使用进程池
。下面是子进程
(child_process.js)的实现逻辑:
// node/child_process.js const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { if (message === 'START') { // 开始干活 hardWork() // 干完活就通知子进程 process.send(message) } })
子进程的代码也很简单,它在启动后会通过process.on
的方式监听来自父进程的消息,在接收到开始命令后进行CPU密集型
的计算,得出结果后返回给父进程。
运行上面master_process.js
的代码,我们可以发现即使调用了/hard_work
接口,我们还是可以任意调用/easy_work
接口并且马上得到响应的,此处没有截图,过程大家脑补一下就可以了。
除了fork
函数,child_process
还提供了诸如exec
和spawn
等函数来启动子进程,并且这些进程可以执行任何的shell
命令而不只是局限于Node脚本,有兴趣的读者后面可以通过官方文档了解一下,这里就不过多介绍了。
Child Process的优缺点
最后让我们来总结一下Child Process
的优点有哪些:
-
灵活
:不只局限于Node进程,我们可以在子进程里面执行任何的shell
命令。这个其实是一个很大的优点,假如我们的CPU密集型操作是用其它语言实现
的(例如c语言处理图像),而我们不想使用Node或者C++ Binding重新实现一遍的话我们就可以通过shell
命令调用其它语言的程序,并且通过标准输入输出
和它们进行通信从而得到结果。 -
细粒度的资源控制
:不像Cluster Module,Child Process方案可以按照实际对CPU密集型计算的需求大小动态调整子进程的个数,做到资源的细粒度控制,因此它理论上
是可以解决Cluster Module解决不了的CPU密集型接口调用频繁
的问题。
不过Child Process的缺点也很明显:
-
资源消耗巨大
:上面说它可以对资源进行细粒度控制
的优点时,也说了它只是理论上
可以解决CPU密集型接口频繁调用的问题
,这是因为实际场景下我们的资源也是有限的
,而每一个Child Process都是一个独立的操作系统进程,会消耗巨大的资源。因此对于频繁调用的接口我们需要采取能耗更低的方案也就是下面我会说的Worker Thread
。 -
进程通信麻烦
:如果启动的子进程也是Node应用的话还好办点,因为有内置的API
来和父进程通信,如果子进程不是Node应用的话,我们只能通过标准输入输出
或者其它方式来进行进程间通信,这是一件很麻烦的事。
Worker Thread
无论是Cluster Module还是Child Process其实都是基于子进程的,它们都有一个巨大的缺点就是资源消耗大
。为了解决这个问题Node从v10.5.0版本(v12.11.0 stable)开始就支持了worker_threads
模块,worker_thread是Node对于CPU密集型操作
的轻量级的线程解决方案
。
概念介绍
Node的Worker Thread
和其它语言的thread是一样的,那就是并发
地运行你的代码。这里要注意是并发
而不是并行
。并行
只是意味着一段时间内多件事情同时发生
,而并发
是某个时间点多件事情同时发生
。一个典型的并行
例子就是React的Fiber架构
,因为它是通过时分复用
的方式来调度不同的任务来避免React渲染
阻塞浏览器的其它行为的,所以本质上它所有的操作还是在同一个操作系统线程
执行的。不过这里值得注意的是:虽然并发
强调多个任务同时执行,在单核CPU的情况下,并发会退化为并行
。这是因为CPU同一个时刻只能做一件事,当你有多个线程需要执行的话
就需要通过资源抢占
的方式来时分复用
执行某些任务。不过这都是操作系统需要关心的东西,和我们没什么关系了。
上面说了Node的Worker Thead和其他语言线程的thread类似的地方,接着我们来看一下它们不一样的地方。如果你使用过其它语言的多线程编程方式,你会发现Node的多线程和它们很不一样,因为Node多线程数据共享起来
实在是太麻烦了
!Node是不允许你通过共享内存变量
的方式来共享数据的,你只能用ArrayBuffer
或者SharedArrayBuffer
的方式来进行数据的传递和共享。虽然说这很不方便,不过这也让我们不需要过多考虑多线程环境下数据安全等一系列问题
,可以说有好处也有坏处吧。
接着我们来看一下如何使用Worker Thread
来处理上面的CPU密集型任务,先看一下主线程(master_thread.js)的代码:
// node/master_thread.js const { Worker } = require('worker_threads') const http = require('http') const url = require('url') const server = http.createServer((req, resp) => { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { // 对于每一个hard_work接口,我们都启动一个子线程来处理 const worker = new Worker('./child_process') // 告诉子线程开始任务 worker.postMessage('START') worker.on('message', () => { // 在收到子线程回复后返回结果给客户端 resp.write('hard work') resp.end() }) } else if (urlParsed.pathname === '/easy_work') { // 其它简单操作都在主线程执行 resp.write('easy work') resp.end() } else { resp.end() } }) server.listen(8080, () => { console.log('server is up...') })
在上面的代码中,我们的服务器每次接收到/hard_work
请求都会通过new Worker
的方式启动一个Worker
线程来处理,在worker处理完任务之后我们再将结果返回给客户端,这个过程是异步的。接着再看一下子线程
(worker_thead.js)的代码实现:
// node/worker_thread.js const { parentPort } = require('worker_threads') const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { if (message === 'START') { hardWork() parentPort.postMessage() } })
在上面的代码中,worker thread在接收到主线程的命令后开始执行CPU密集型
操作,最后通过parentPort.postMessage
的方式告知父线程任务已经完成,从API上看父子线程通信还是挺方便的。
Worker Thread的優缺點
最後我們還是總結一下Worker Thread的優缺點。首先我覺得它的優點是:
-
資源消耗小
:不同於Cluster Module和Child Process基於進程的方式,Worker Thread是基於更輕量級的線程的,所以它的資源開銷是相對較小的
。不過麻雀雖小五臟俱全
,每個Worker Thread
都是有自己獨立的v8引擎實例
和事件循環
系統的。這也就是說即使主線程卡死
我們的Worker Thread
也是可以繼續工作的,基於這個其實我們可以做很多有趣的事情。 -
父子線程通信方便高效
:和前面兩種方式不一樣,Worker Thread不需要透過IPC通信,所有資料都是在進程內部實現共享和傳遞的。
不過Worker Thread也不是完美的:
-
#線程隔離性低
:由於子執行緒不是在一個獨立的環境
執行的,所以某個子線程掛了還是會影響到其它線程,在這種情況下,你需要做一些額外的措施來保護其餘線程不受影響。 -
線程資料共享實現麻煩
:和其它後端語言比起來,Node的資料共享還是比較麻煩的,不過這其實也避免了它需要考慮很多多線程下資料安全的問題。
總結
在本篇文章中我為大家介紹了Node為什麼適合做I/O密集型應用而很難處理CPU密集型任務的原因,並且為大家提供了三個可選方案來在實際開發中處理CPU密集型任務。每個方案其實都有利有弊,我們一定要根據實際情況進行選擇,永遠不要為了要用某個技術而一定要採取某個方案
。
更多node相關知識,請造訪:nodejs 教學!
以上是淺析Node處理CPU密集型任務的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

JavaScript是現代網站的核心,因為它增強了網頁的交互性和動態性。 1)它允許在不刷新頁面的情況下改變內容,2)通過DOMAPI操作網頁,3)支持複雜的交互效果如動畫和拖放,4)優化性能和最佳實踐提高用戶體驗。

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

VSCode Windows 64位元 下載
微軟推出的免費、功能強大的一款IDE編輯器

ZendStudio 13.5.1 Mac
強大的PHP整合開發環境

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

記事本++7.3.1
好用且免費的程式碼編輯器

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),