鋒利的NodeJS之NodeJS多執行緒

freephp發表於2021-04-10

最近剛好有朋友在問Node.js多執行緒的問題,我總結了一下,可以考慮使用原始碼包裡面的worker_threads或者第三方的模組來實現。
首先明確一下多執行緒在Node.js中的概念,然後在聊聊worker_threads的用法。天生非同步,真心強大。

  1. 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”字串。
  1. 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'.
}
  1. 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();
  });
}

相關文章