最近剛好有朋友在問Node.js多執行緒的問題,我總結了一下,可以考慮使用原始碼包裡面的worker_threads或者第三方的模組來實現。
首先明確一下多執行緒在Node.js中的概念,然後在聊聊worker_threads的用法。天生非同步,真心強大。
- Node.js多執行緒概述
有人可能會說,Node.js雖然是單執行緒的,但是可以利用迴圈事件(Event Loop)l來實現併發執行任務。追究其本質,NodeJs實際上使用了兩種不同的執行緒,一個是用於處理迴圈事件的主執行緒一個是工作池(Worker pool)裡面的一些輔助執行緒。關於這兩種執行緒主要功能和關係如圖1所示。
圖1 Node.js執行緒圖
所以從本質上來講,NodeJs並不是真正的原生多執行緒,而是利用迴圈事件來實現高效併發執行任務。要做到真正的多執行緒,需要依賴其他模組或者第三方庫。
2. Worker_threads是Node.js官方推薦的實現真正多執行緒的模組,有官方技術團隊進行長期維護。Worker_threads不需要單獨安裝,它位於Node.js原始碼中,具體位置是lib/worker_threads.js。worker_threads模組允許使用並行執行JavaScript的執行緒,使用也非常方便,只需要引入該模組即可,程式碼如下。
const worker = require('worker_threads');
與child_process或cluster不同,worker_threads可以共享記憶體。它們通過傳輸ArrayBuffer例項或共享SharedArrayBuffer例項來實現。
官網上給了一個完整的例子,如下所示。
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', message => console.log(message));
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with exit code ${code}`));
});
});
};
} else {
const { parse } = require('som-parse-libary');
const script = workerData;
parentPort.postMessage(parse(script));
}
筆者對以上程式碼開始解析,重點概念如下所示:
Worker該類代表一個獨立的js執行執行緒。
isMainThead一個布林值,當前程式碼是否執行在Worker執行緒中。
parentPortMessagePort物件,如果當前執行緒是個生成的Worker執行緒,則允許和父執行緒通訊。
workerData一個可以傳遞給執行緒建構函式的任何js資料的的複製資料。
Worker_theads還提供了很多實用的API,整理如下所示。
1.worker.getEnvironmentData(key)
可以獲取環境變數,先使用setEnvironmentData來設定環境變數,然後再使用g
etEnvironmentData來獲取。
舉一個簡單的例子,程式碼如下所示。
const {
Worker,
isMainThread,
setEnvironmentData,
getEnvironmentData,
} = require('worker_threads');
if (isMainThread) {
setEnvironmentData('Hi', 'Node.js!');
const worker = new Worker(__filename);
} else {
console.log(getEnvironmentData('Hi'));.
}
執行這段程式碼,可以在控制檯列印出“Node.js”字串。
- isMainThread
isMainThread可以用來判斷該程式是不是主執行緒,如果是主執行緒,則返回true,否則返回false。下面編寫一個巢狀worker的程式碼,用於展示。
const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
console.log("This is a main thread\r\n");
// This re-loads the current file inside a Worker instance.
new Worker(__filename);
} else {
console.log('Inside Worker!');
console.log(isMainThread); // Prints 'false'.
}
- MessageChannel和相關用法
MessageChannel是worker_threads提供的一個雙向非同步的訊息通訊通道。下面這段程式碼就展示了兩個MessagePort物件互相傳遞訊息的過程,我們如果想主動結束某個Channel,那麼可以使用close事件來完成。
const {MessageChannel} = require('worker_threads');
const {port1, port2} = new MessageChannel();
// port1給port2傳送資訊
port1.postMessage({carName: 'BYD'});
port2.on('message', (message) => {
console.log("I receive message is ", message);
})
// port2給port1傳送資訊
port2.postMessage({personality: "Brave"});
port1.on('message', (message) => {
console.log("I receive message is ", message);
});
執行上面的程式碼,可以在控制檯看到如下輸出:
I receive message is { personality: 'Brave' }
I receive message is { carName: 'BYD' }
port.on(‘message’)方法是利用被動等待的方式接收事件,如果想手動接收資訊可以使用receiveMessageOnPort方法,指定從某個port接收訊息,如下所示。
const { MessageChannel, receiveMessageOnPort } = require('worker_threads');
const {port1, port2} = new MessageChannel();
port1.postMessage({Name: "freePHP"});
let result = receiveMessageOnPort(port2);
console.log(result);
let result2 = receiveMessageOnPort(port2);
console.log(result2);
執行上面的程式碼,可以得到如下輸出。
{ message: { Name: 'freePHP' } }
undefined
從結果可以看出,receiveMessageOnPort可以指定從另一個MessagePort物件獲取訊息,是一次消耗訊息。
實際工作中,我們不可能只使用單個執行緒來完成任務,所以需要建立執行緒池來維護和管理worker thread物件。為了簡化執行緒池的實現,假設只會傳遞一個woker指令碼作為引數,具體實現如下所示。需要單獨安裝async_hooks模組,它用於非同步載入資源。
const { AsyncResource } = require('async_hooks'); // 用於非同步載入資源
const { EventEmitter } = require('events');
const path = require('path');
const { Worker } = require('worker_threads');
const kTaskInfo = Symbol('kTaskInfo');
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent');
class WorkerPoolTaskInfo extends AsyncResource {
constructor(callback) {
super('WorkerPoolTaskInfo');
this.callback = callback;
}
done(err, result) {
this.runInAsyncScope(this.callback, null, err, result);
this.emitDestroy(); // 只會被執行一次
}
}
class WorkerPool extends EventEmitter {
constructor(numThreads) {
super();
this.numThreads = numThreads;
this.workers = [];
this.freeWorkers = [];
for (let i = 0; i < numThreads; i++)
this.addNewWorker();
}
/**
* 新增新的執行緒
*/
addNewWorker() {
const worker = new Worker(path.resolve(__dirname, 'task2.js'));
worker.on('message', (result) => {
// 如果成功狀態,則將回撥傳給runTask方法,然後worker移除TaskInfo標記。
worker[kTaskInfo].done(null, result);
worker[kTaskInfo] = null;
//
this.freeWorkers.push(worker);
this.emit(kWorkerFreedEvent);
});
worker.on('error', (err) => {
// 報錯後呼叫回撥
if (worker[kTaskInfo])
worker[kTaskInfo].done(err, null);
else
this.emit('error', err);
// 移除一個worker,然後啟動一個新的worker來代替當前的worker
this.workers.splice(this.workers.indexOf(worker), 1);
this.addNewWorker();
});
this.workers.push(worker);
this.freeWorkers.push(worker);
this.emit(kWorkerFreedEvent);
}
/**
* 執行任務
* @param task
* @param callback
*/
runTask(task, callback) {
if (this.freeWorkers.length === 0) {
this.once(kWorkerFreedEvent, () => this.runTask(task, callback));
return;
}
const worker = this.freeWorkers.pop();
worker[kTaskInfo] = new WorkerPoolTaskInfo(callback);
worker.postMessage(task);
}
/**
* 關閉執行緒
*/
close() {
for (const worker of this.workers) {
worker.terminate();
}
}
}
module.exports = WorkerPool;
其中task2.js是定義好的一個計算兩個數字相加的指令碼,內容如下。
const { parentPort } = require('worker_threads');
parentPort.on('message', (task) => {
parentPort.postMessage(task.a + task.b);
});
呼叫這個執行緒池非常簡單,用例如下所示。
const WorkerPool = require('./worker_pool.js');
const os = require('os');
const pool = new WorkerPool(os.cpus().length);
let finished = 0;
for (let i = 0; i < 10; i++) {
pool.runTask({ a: 42, b: 100 }, (err, result) => {
console.log(i, err, result);
if (++finished === 10)
pool.close();
});
}