Node 單執行緒究竟是怎麼回事?Node多執行緒又是怎麼回事?希望這篇文章能夠講清楚。
閱讀時間大約10~13min
本文測試使用環境:
系統:macOS Mojave 10.14.2
CPU:4 核 2.3 GHz
Node: 10.15.1
從 Node 執行緒說起
一般人理解 Node 是單執行緒的,所以 Node 啟動後執行緒數應該為 1,我們做實驗看一下。
setInterval(() => {
console.log(new Date().getTime())
}, 3000)
複製程式碼
可以看到 Node 程式佔用了 7 個執行緒。為什麼會有 7 個執行緒呢?
我們都知道,Node 中最核心的是 v8 引擎,在 Node 啟動後,會建立 v8 的例項,這個例項是多執行緒的。
- 主執行緒:編譯、執行程式碼。
- 編譯/優化執行緒:在主執行緒執行的時候,可以優化程式碼。
- 分析器執行緒:記錄分析程式碼執行時間,為 Crankshaft 優化程式碼執行提供依據。
- 垃圾回收的幾個執行緒。
所以大家常說的 Node 是單執行緒的指的是 JavaScript 的執行是單執行緒的,但 Javascript 的宿主環境,無論是 Node 還是瀏覽器都是多執行緒的。
Node 有兩個編譯器:
full-codegen:簡單快速地將 js 編譯成簡單但是很慢的機械碼。
Crankshaft:比較複雜的實時優化編譯器,編譯高效能的可執行程式碼。
某些非同步 IO 會佔用額外的執行緒
還是上面那個例子,我們在定時器執行的同時,去讀一個檔案:
const fs = require('fs')
setInterval(() => {
console.log(new Date().getTime())
}, 3000)
fs.readFile('./index.html', () => {})
複製程式碼
執行緒數量變成了 11 個,這是因為在 Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集計算(Zlib,Crypto)會啟用 Node 的執行緒池,而執行緒池預設大小為 4,因為執行緒數變成了 11。
我們可以手動更改執行緒池預設大小:
process.env.UV_THREADPOOL_SIZE = 64
複製程式碼
一行程式碼輕鬆把執行緒變成 71。
cluster 是多執行緒嗎?
Node 的單執行緒也帶來了一些問題,比如對 cpu 利用不足,某個未捕獲的異常可能會導致整個程式的退出等等。因為 Node 中提供了 cluster 模組,cluster 實現了對 child_process 的封裝,通過 fork 方法建立子程式的方式實現了多程式模型。比如我們最常用到的 pm2 就是其中最優秀的代表。
我們看一個 cluster 的 demo:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主程式 ${process.pid} 正在執行`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作程式 ${worker.process.pid} 已退出`);
});
} else {
// 工作程式可以共享任何 TCP 連線。
// 在本例子中,共享的是 HTTP 伺服器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
}).listen(8000);
console.log(`工作程式 ${process.pid} 已啟動`);
}
複製程式碼
這個時候看下活動監視器:
一共有 9 個程式,其中一個主程式,cpu 個數 x cpu 核數 = 2 x 4 = 8 個 子程式。
所以無論 child_process 還是 cluster,都不是多執行緒模型,而是多程式模型。雖然開發者意識到了單執行緒模型的問題,但是沒有從根本上解決問題,而且提供了一個多程式的方式來模擬多執行緒。從前面的實驗可以看出,雖然 Node (V8)本身是具有多執行緒的能力的,但是開發者並不能很好的利用這個能力,更多的是由 Node 底層提供的一些方式來使用多執行緒。Node 官方說:
You can use the built-in Node Worker Pool by developing a C++ addon. On older versions of Node, build your C++ addon using NAN, and on newer versions use N-API. node-webworker-threads offers a JavaScript-only way to access Node’s Worker Pool.
但是對於 JavaScript 開發者,一直沒有一個標準的、好用的方式來使用 Node 的多執行緒能力。
真 - Node 多執行緒
直到 Node 10.5.0 的釋出,官方才給出了一個實驗性質的模組 worker_threads 給 Node 提供真正的多執行緒能力。
先看下簡單的 demo:
const {
isMainThread,
parentPort,
workerData,
threadId,
MessageChannel,
MessagePort,
Worker
} = require('worker_threads');
function mainThread() {
for (let i = 0; i < 5; i++) {
const worker = new Worker(__filename, { workerData: i });
worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });
worker.on('message', msg => {
console.log(`main: receive ${msg}`);
worker.postMessage(msg + 1);
});
}
}
function workerThread() {
console.log(`worker: workerDate ${workerData}`);
parentPort.on('message', msg => {
console.log(`worker: receive ${msg}`);
}),
parentPort.postMessage(workerData);
}
if (isMainThread) {
mainThread();
} else {
workerThread();
}
複製程式碼
上述程式碼在主執行緒中開啟五個子執行緒,並且主執行緒向子執行緒傳送簡單的訊息。
由於 worker_thread 目前仍然處於實驗階段,所以啟動時需要增加 --experimental-worker
flag,執行後觀察活動監視器:
不多不少,正好多了五個子執行緒。
worker_thread 模組
worker_thread 核心程式碼
worker_thread 模組中有 4 個物件和 2 個類。
- isMainThread: 是否是主執行緒,原始碼中是通過
threadId === 0
進行判斷的。 - MessagePort: 用於執行緒之間的通訊,繼承自 EventEmitter。
- MessageChannel: 用於建立非同步、雙向通訊的通道例項。
- threadId: 執行緒 ID。
- Worker: 用於在主執行緒中建立子執行緒。第一個引數為 filename,表示子執行緒執行的入口。
- parentPort: 在 worker 執行緒裡是表示父程式的 MessagePort 型別的物件,在主執行緒裡為 null
- workerData: 用於在主程式中向子程式傳遞資料(data 副本)
來看一個程式通訊的例子:
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('received:', value);
});
} else {
parentPort.once('message', (value) => {
assert(value.hereIsYourPort instanceof MessagePort);
value.hereIsYourPort.postMessage('the worker is sending this');
value.hereIsYourPort.close();
});
}
複製程式碼
更多詳細用法可以檢視官方文件。
多程式 vs 多執行緒
根據大學課本上的說法:“程式是資源分配的最小單位,執行緒是CPU排程的最小單位”,這句話應付考試就夠了,但是在實際工作中,我們還是要根據需求合理選擇。
下面對比一下多執行緒與多程式:
屬性 | 多程式 | 多執行緒 | 比較 |
---|---|---|---|
資料 | 資料共享複雜,需要用IPC;資料是分開的,同步簡單 | 因為共享程式資料,資料共享簡單,同步複雜 | 各有千秋 |
CPU、記憶體 | 佔用記憶體多,切換複雜,CPU利用率低 | 佔用記憶體少,切換簡單,CPU利用率高 | 多執行緒更好 |
銷燬、切換 | 建立銷燬、切換複雜,速度慢 | 建立銷燬、切換簡單,速度很快 | 多執行緒更好 |
coding | 編碼簡單、除錯方便 | 編碼、除錯複雜 | 多程式更好 |
可靠性 | 程式獨立執行,不會相互影響 | 執行緒同呼吸共命運 | 多程式更好 |
分散式 | 可用於多機多核分散式,易於擴充套件 | 只能用於多核分散式 | 多程式更好 |
上述比較僅表示一般情況,並不絕對。
work_thread 讓 Node 有了真正的多執行緒能力,算是不小的進步。
前往【IVWEB社群】公眾號檢視更多幹貨文章