Node.js 傳統上是單線程的,並依賴非同步 I/O 來實現並發。雖然此模型擅長處理 I/O 密集型工作負載,但 CPU 密集型操作會阻止事件循環並降低應用程式回應能力。在 Node.js 12 中穩定的工作執行緒透過在單獨的 V8 隔離中執行 JavaScript 來在單一進程中提供真正的平行執行。本文介紹了在生產中使用工作執行緒的實用模式。
工人生命週期和溝通
建立工作執行緒需要一個單獨的 JavaScript 文件,該文件在自己的 V8 隔離中執行,並具有自己的堆和事件循環。
// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js", {
workerData: { input: largeDataset },
});
worker.on("message", (result) => {
logger.info({ result }, "Worker completed");
});
worker.on("error", (err) => {
logger.error({ err }, "Worker failed");
});
worker.on("exit", (code) => {
if (code !== 0) logger.error({ exitCode: code }, "Worker crashed");
});
// worker.js
const { parentPort, workerData } = require("worker_threads");
const result = processData(workerData.input);
parentPort.postMessage(result);
工作人員透過結構化克隆進行通信,該克隆支援物件、陣列、Map、Sets、RegExp、Date 和 ArrayBuffers。對於大型二進位數據,請使用可傳輸物件來避免複製開銷 - 來源緩衝區在傳輸後會被中和。
使用 SharedArrayBuffer 共享記憶體
對於訊息複製成本過高的高吞吐量場景,SharedArrayBuffer 提供線程之間的零複製共享記憶體。必須使用 Atomics 操作來協調訪問,以防止競爭條件。
// main.js
const sharedBuffer = new SharedArrayBuffer(4 * 1024 * 1024); // 4 MB
const sharedArray = new Int32Array(sharedBuffer);
const worker = new Worker("./worker.js");
worker.postMessage({ sharedBuffer });
// Wait for worker to signal completion
Atomics.wait(sharedArray, 0, 0);
const result = sharedArray[1];
// worker.js
const { parentPort, workerData } = require("worker_threads");
const sharedArray = new Int32Array(workerData.sharedBuffer);
// Perform computation directly on shared memory
sharedArray[1] = computeResult();
Atomics.store(sharedArray, 0, 1); // Signal completion
Atomics.notify(sharedArray, 0);
| 機制 | 開銷 | 使用案例 |
|---|---|---|
postMessage(結構化複製) | 每次通話中等 | 大多數任務,複雜的物件 |
| 可轉讓物品 | 低(零拷貝) | 大緩衝區、二進位資料 |
SharedArrayBuffer + Atomics | 最小 | 高頻更新,流資料 |
執行緒池實現
為每個任務建立新的 Worker 實例會產生啟動成本。線程池維護一組可重複使用的工作線程,在它們之間有效率地分配任務。
class WorkerPool {
constructor(workerPath, numThreads = os.cpus().length) {
this.workers = [];
this.queue = [];
this.activeCount = 0;
for (let i = 0; i < numThreads; i++) {
const worker = new Worker(workerPath);
worker.on("message", (result) => this._complete(worker, result));
worker.on("error", (err) => this._fail(worker, err));
this.workers.push({ worker, busy: false });
}
}
execute(task) {
return new Promise((resolve, reject) => {
const available = this.workers.find((w) => !w.busy);
if (available) {
available.busy = true;
available.worker.postMessage(task);
available.resolve = resolve;
available.reject = reject;
} else {
this.queue.push({ task, resolve, reject });
}
});
}
_complete(worker, result) {
worker.resolve(result);
this._next(worker);
}
_next(worker) {
if (this.queue.length > 0) {
const next = this.queue.shift();
worker.postMessage(next.task);
worker.resolve = next.resolve;
worker.reject = next.reject;
} else {
worker.busy = false;
}
}
}
池大小應與 CPU 核心數相符。超額訂閱的工作執行緒數多於核心數會增加上下文切換開銷,而不會增加吞吐量。
用例:影像處理和資料轉換
調整大小、格式轉換和過濾等影像處理操作受 CPU 限制,並且在主執行緒上執行時會阻塞事件循環。將它們卸載到工作線程可以保持伺服器的回應能力。
// image-worker.js
const sharp = require("sharp");
const { parentPort, workerData } = require("worker_threads");
sharp(workerData.input)
.resize(800, 600)
.jpeg({ quality: 80 })
.toBuffer()
.then((output) => parentPort.postMessage(output));
工作執行緒也擅長處理 CPU 密集型資料任務:
- 大負載的 JSON 解析與驗證
- CSV 和 Excel 文件處理
- 使用 zlib 或 brotli 進行資料壓縮和解壓
- 使用 bcrypt 或 argon2 進行密碼雜湊
- PDF 產生和渲染
基準測試通常顯示,當 CPU 繁重的工作被卸載給工作執行緒時,p99 事件循環延遲會提高 5-10 倍,因為主執行緒仍然可以自由地處理傳入請求。
與子進程和集群的比較
| 特色 | 工作線程 | 子進程 | 集群 |
|---|---|---|---|
| 記憶體模型 | 共享(同一進程) | 獨立流程 | 獨立流程 |
| 啟動時間 | 〜5-10 毫秒 | ~20-50 毫秒 | ~20-50 毫秒 |
| 通訊 | 結構化複製+共享記憶體 | 系列化IPC | 系列化IPC |
| 最適合 | CPU 密集型任務 | 隔離,本機外掛程式 | I/O 密集型 HTTP 工作負載 |
cluster 模組分叉多個 Node.js 進程來處理 HTTP 請求。工作線程透過處理每個集群工作線程內的 CPU 密集型工作來補充集群:
if (cluster.isPrimary) {
for (let i = 0; i < numCPUs; i++) cluster.fork();
} else {
const pool = new WorkerPool("./cpu-worker.js");
app.get("/process", async (req, res) => {
const result = await pool.execute(req.query.data);
res.json(result);
});
}
監控與調試
工作執行緒需要特定的監視方法。監聽生命週期事件,追蹤工作執行緒內部的記憶體使用情況,並使用 --inspect-brk 進行 Chrome DevTools 偵錯。實施運行狀況檢查,驗證工作人員是否回應並重新啟動任何已崩潰或無回應的工作人員。使用記錄訊息中的相關 ID 將工作程序輸出與特定請求關聯起來。
結論
工作執行緒透過為 CPU 密集型工作負載提供真正的平行執行來填補 Node.js 中的一個關鍵空白。透過精心設計的線程池、具有 Atomics 同步的共享記憶體以及仔細的任務選擇,您可以顯著提高應用程式吞吐量,同時保持事件循環響應。首先識別應用程式中的 CPU 密集型操作,實現具有適當錯誤處理的線程池,並對延遲改進進行基準測試以驗證投資。

