Nodejs一直以單線程異步IO著稱,擅長IO密集型操作,不擅長CPU密集型操作。但是,新版的Nodejs,在不斷彌補這方面的短板。
一、CPU密集型(CPU-bound)
CPU密集型也叫計算密集型,指的是系統的硬盤、內存性能相對CPU要好很多,此時,系統運作大部分的狀況是CPU Loading 100%,CPU要讀/寫I/O(硬盤/內存),I/O在很短的時間就可以完成,而CPU還有許多運算要處理,CPU Loading很高。
在多重程序系統中,大部份時間用來做計算、邏輯判斷等CPU動作的程序稱之CPU bound。例如一個計算圓周率至小數點一千位以下的程序,在執行的過程當中絕大部份時間用在三角函數和開根號的計算,便是屬於CPU bound的程序。
CPU bound的程序一般而言CPU佔用率相當高。這可能是因爲任務本身不太需要訪問I/O設備,也可能是因爲程序是多線程實現因此屏蔽掉了等待I/O的時間。
二、IO密集型(I/O bound)
IO密集型指的是系統的CPU性能相對硬盤、內存要好很多,此時,系統運作,大部分的狀況是CPU在等I/O (硬盤/內存) 的讀/寫操作,此時CPU Loading並不高。
I/O bound的程序一般在達到性能極限時,CPU佔用率仍然較低。這可能是因爲任務本身需要大量I/O操作,而pipeline做得不是很好,沒有充分利用處理器能力。
三、CPU密集型 vs IO密集型
我們可以把任務分爲計算密集型和IO密集型。
計算密集型任務的特點是要進行大量的計算,消耗CPU資源,比如計算圓周率、對視頻進行高清解碼等等,全靠CPU的運算能力。這種計算密集型任務雖然也可以用多任務完成,但是任務越多,花在任務切換的時間就越多,CPU執行任務的效率就越低,所以,要最高效地利用CPU,計算密集型任務同時進行的數量應當等於CPU的核心數。
計算密集型任務由於主要消耗CPU資源,因此,代碼運行效率至關重要。Python這樣的腳本語言運行效率很低,完全不適合計算密集型任務。對於計算密集型任務,最好用C語言編寫。
第二種任務的類型是IO密集型,涉及到網絡、磁盤IO的任務都是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因爲IO的速度遠遠低於CPU和內存的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。常見的大部分任務都是IO密集型任務,比如Web應用。
IO密集型任務執行期間,99%的時間都花在IO上,花在CPU上的時間很少,因此,用運行速度極快的C語言替換用Python這樣運行速度極低的腳本語言,完全無法提升運行效率。對於IO密集型任務,最合適的語言就是開發效率最高(代碼量最少)的語言,腳本語言是首選,C語言最差。
總之,計算密集型程序適合C語言多線程,I/O密集型適合腳本語言開發的多線程。
四、進程 vs. 線程
我們介紹了多進程和多線程,這是實現多任務最常用的兩種方式。現在,我們來討論一下這兩種方式的優缺點。
首先,要實現多任務,通常我們會設計Master-Worker模式,Master負責分配任務,Worker負責執行任務,因此,多任務環境下,通常是一個Master,多個Worker。
如果用多進程實現Master-Worker,主進程就是Master,其他進程就是Worker。
如果用多線程實現Master-Worker,主線程就是Master,其他線程就是Worker。
多進程模式最大的優點就是穩定性高,因爲一個子進程崩潰了,不會影響主進程和其他子進程。(當然主進程掛了所有進程就全掛了,但是Master進程只負責分配任務,掛掉的概率低)著名的Apache最早就是採用多進程模式。
多進程模式的缺點是創建進程的代價大,在Unix/Linux系統下,用fork
調用還行,在Windows下創建進程開銷巨大。另外,操作系統能同時運行的進程數也是有限的,在內存和CPU的限制下,如果有幾千個進程同時運行,操作系統連調度都會成問題。
多線程模式通常比多進程快一點,但是也快不到哪去,而且,多線程模式致命的缺點就是任何一個線程掛掉都可能直接造成整個進程崩潰,因爲所有線程共享進程的內存。在Windows上,如果一個線程執行的代碼出了問題,你經常可以看到這樣的提示:“該程序執行了非法操作,即將關閉”,其實往往是某個線程出了問題,但是操作系統會強制結束整個進程。
在Windows下,多線程的效率比多進程要高,所以微軟的IIS服務器默認採用多線程模式。由於多線程存在穩定性的問題,IIS的穩定性就不如Apache。爲了緩解這個問題,IIS和Apache現在又有多進程+多線程的混合模式,真是把問題越搞越複雜。
在 Node 10.5.0,官方給出了一個實驗性質的模塊 worker_threads 給 Node 提供了真正的多線程能力
在 Node.js 12.11.0,worker_threads 模塊正式進入穩定版
至此,Nodejs算是了真正的多線程能力。進程是資源分配的最小單位,線程是CPU調度的最小單位。
1. Nodejs多線程種類
Node.js 中有三類線程 (child_process 和 cluster 的實現均爲進程)
1. event loop的主線程
2. libuv的異步I/O線程池
3. worker_threads的線程
2. worker_threads的作用
worker_thread 相比進程的方案,他們與父線程公用一個進程 ID,可輕鬆與另一個線程共享內存(ArrayBuffer 或 SharedArrayBuffer),從而避免了額外的序列化和反序列化開銷。
但是 Worker Threads 對於 I/O 密集型操作是沒有太大的幫助的,因爲異步的 I/O 操作比 worker 更有效率,Wokers 的主要作用是用於提升對於 CPU 密集型操作的性能。
3. worker_threads的線程
3.1 線程的基本用法
worker_threads也是master-work模型,有主線程和工作線程之分。
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); if (isMainThread) { module.exports = function parseJSAsync(script) { return new Promise((resolve, reject) => { const worker = new Worker(__filename, { workerData: script }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) reject(new Error(`工作線程使用退出碼 ${code} 停止`)); }); }); }; } else { const { parse } = require('一些 js 解析庫'); const script = workerData; parentPort.postMessage(parse(script)); }
3.2 線程間的通信
1. 共享內存
與child_process和cluster的進程不同,線程之間可以共享內存。使用ArrayBuffer或SharedArrayBuffer
2. parentPort
主要用於主子線程通信,通過經典的 on('message'), postMessage形式
if (isMainThread) { const worker = new Worker(__filename); worker.once('message', (message) => { console.log(message); // Prints 'Hello, world!'. }); worker.postMessage('Hello, world!'); } else { // When a message from the parent thread is received, send it back: parentPort.once('message', (message) => { parentPort.postMessage(message); }); }
3. MessageChannel
與 Web 工作線程和 cluster 模塊一樣,可以通過線程間的消息傳遞來實現雙向通信。 在內部,一個 Worker 具有一對內置的 MessagePort,在創建該 Worker 時它們已經相互關聯。 雖然父端的 MessagePort 對象沒有直接公開,但其功能是通過父線程的 Worker 對象上的 worker.postMessage() 和 worker.on('message') 事件公開的。
要創建自定義的消息傳遞通道(建議使用默認的全局通道,因爲這樣可以促進關聯點的分離),用戶可以在任一線程上創建一個 MessageChannel 對象,並將該 MessageChannel 上的 MessagePort 中的一個通過預先存在的通道傳給另一個線程,例如全局的通道。
const assert = require('assert'); const { Worker, MessageChannel, MessagePort, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { const worker = new Worker(__filename); const subChannel = new MessageChannel(); worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); subChannel.port2.on('message', (value) => { console.log('接收到:', value); }); } else { parentPort.once('message', (value) => { assert(value.hereIsYourPort instanceof MessagePort); value.hereIsYourPort.postMessage('工作線程正在發送此消息'); value.hereIsYourPort.close(); }); }