相信大家都知道 是一個單執行緒程式,使用了 Event Loop 可以做到多並行。可惜這是不完全正確的。
那麼為什麼說 Node.js 不是完全的單執行緒的程式呢?
所有我們自己寫的 Javsacript,V8, event loop都跑在同一個執行緒裡面,也就是 main thrad。
哎嗨,這不正說明 node 是單執行緒的嗎?
但是也許你不知道 node 有很多模組背後都是 C++ code。
雖然 node 沒有給使用者暴露控制 thread 的許可權,但是 C++ 是可以使用多執行緒的。
那麼什麼時候 node 會使用多執行緒呢?
如果一個 node 方法,背後呼叫C++的同步方法,那麼都是跑在 main thread 裡面的。
如果一個 node 方法,背後呼叫C++的非同步方法,有時候不是跑在 main thread 裡面的。
Talk is cheap, show me the code.
這裡 crypto
相關模組,很多是 C++ 寫的。下面一段程式是計算hash的函數,一般用來儲存密碼。
import { pbkdf2Sync } from "crypto"; const startTime = Date.now(); let index = 0; for (index = 0; index < 3; index++) { pbkdf2Sync("secret", "salt", 100000, 64, "sha512"); const endTime = Date.now(); console.log(`${index} time, ${endTime - startTime}`); } const endTime = Date.now(); console.log(`in the end`);
輸出的時間,
0 time, 44 1 time, 90 2 time, 134 in the end
可以看到每次大概都是花費~45ms,程式碼 main thread 上順序執行。
注意最後的輸出是誰? 注意這裡一次 hash 在我的 cpu 需要~45ms。
import { cpus } from "os"; import { pbkdf2 } from "crypto"; console.log(cpus().length); let startTime = console.time("time-main-end"); for (let index = 0; index < 4; index++) { startTime = console.time(`time-${index}`); pbkdf2("secret", `salt${index}`, 100000, 64, "sha512", (err, derivedKey) => { if (err) throw err; console.timeEnd(`time-${index}`); }); } console.timeEnd("time-main-end");
輸出的時間,
time-main-end: 0.31ms time-2: 45.646ms time-0: 46.055ms time-3: 46.846ms time-1: 47.159ms
這裡看到,main thread 早早結束,然而每次計算的時間都是45ms,要知道一個 cpu 計算 hash 的時間是45ms,這裡 node 絕對使用了多個執行緒進行hash計算。
如果我這裡把呼叫次數改成10次,那麼時間如下,可以看到隨著CPU核數的用完,時間也在增加。再一次證明node 絕對使用了多個執行緒進行hash計算。
time-main-end: 0.451ms time-1: 44.977ms time-2: 46.069ms time-3: 50.033ms time-0: 51.381ms time-5: 96.429ms // 注意這裡,從第五次時間開始增加了 time-7: 101.61ms time-4: 113.535ms time-6: 121.429ms time-9: 151.035ms time-8: 152.585ms
雖然這裡證明了,node絕對啟用了多執行緒。但是有一點點小小的問題?我的電腦的CPU是AMD R5-5600U,有6個核心12執行緒啊。但是為什麼時間是從第五次開始增加的呢,node沒有完全利用我的CPU啊?
原因是什麼呢?
Node 使用了預定義的執行緒池,這個執行緒池的大小預設是4.
export UV_THREADPOOL_SIZE=6
讓我們在看一個例子,
import { request } from "https"; const options = { hostname: "www.baidu.com", port: 443, path: "/img/PC_7ac6a6d319ba4ae29b38e5e4280e9122.png", method: "GET", }; let startTime = console.time(`main`); for (let index = 0; index < 15; index++) { startTime = console.time(`time-${index}`); const req = request(options, (res) => { console.log(`statusCode: ${res.statusCode}`); console.timeEnd(`time-${index}`); res.on("data", (d) => { // process.stdout.write(d); }); }); req.on("error", (error) => { console.error(error); }); req.end(); } console.timeEnd("main");
main: 13.927ms time-2: 83.247ms time-4: 89.641ms time-3: 91.497ms time-12: 91.661ms time-5: 94.677ms ..... time-8: 134.026ms time-1: 143.906ms time-13: 140.914ms time-10: 144.088ms
這裡主程式也早早結束了,這裡我啟動 http request 去下載15次圖片,他們花費的時間並沒有成倍增加,似乎不受限於執行緒池/cpu的影響。
為什麼啊??Node 到底有沒有在使用執行緒池啊?
如果 Node 背後的 C++ 的非同步方法,首先會嘗試是否有核心非同步支援,比如這裡網路請是使用 epoll (Linux),如果核心沒有提供非同步方式,Node才會使用自己的執行緒池。。
所以 http 請求雖然是非同步,不過是由核心實現的,等到核心完成後,會通知C++, C++會通知給 main thread 處理callback。
那麼 Node 哪些非同步方法會使用執行緒池呢?哪些不會呢?
原生 Kernal Async
Thread pool
這也是大部分 Node 優化的切入點。
但是這些怎麼和最重要的 Event Loop 結合起來呢?
相信大家都對 Event loop 非常熟悉了。Event loop 好比一個分發員,
如果是遇到普通 javascript 程式或者是 callback,交給 V8 處理。
如果遇到同步方法後背是 C++ 寫的,交給C++,跑在 main thread。
如果遇到非同步方法後背是 C++ 寫的,如果有核心非同步支援,從main thread 交給核心處理。
如果是非同步方法後背是 C++ 寫的,如果沒有核心非同步支援,從 main thread 交給 thread pool。
thread pool 和核心有結果都會把結果返回 event loop,如果註冊的有 javascript callback,就交給V8進行處理。
然後如此迴圈,直到沒有東西可以處理。
所以 Node 不完全是單執行緒程式。
更多node相關知識,請存取:!
以上就是如何理解 Node.js 不是完全的單執行緒的程式(淺析)的詳細內容,更多請關注TW511.COM其它相關文章!