求不更學不動之Node.js多執行緒

farsun發表於2021-09-09


伴隨10.5.0的釋出,Node.js 新增了對多執行緒的實驗性支援(worker_threads模組)。

為什麼需要多執行緒?

Node.js由於JS的執行在單一執行緒,導致CPU密集計算的任務可能會使主執行緒會處於繁忙的狀態,進而影響服務的效能,雖然可以透過child_process模組建立子程式的方式來解決,但是一方面程式之間無法共享記憶體,另一方面建立程式的開銷也不小。所以在10.5.0版本中Node.js提供了worker_threads模組來支援多執行緒,一直以來被人所詬病的不擅長CPU密集計算有望成為歷史。

如何啟用多執行緒?

多執行緒目前仍然處於實驗階段,所以啟動時需要增加--experimental-workerflag才能生效。

如何建立多執行緒?

worker_threads模組中比較重要的幾個類:

MessageChannel: 用於建立非同步、雙向通訊的通道例項。MessageChannel例項包含兩個屬性port1和port2,這兩個屬性都是MessagePort的例項。

MessagePort: 用於表示MessageChannel通道的終端,用於Worker之間傳輸結構化資料、記憶體區域和其他的MessagePort。MessagePort繼承了EventEmitter,因此可以使用postMessage和on方法實現訊息的傳遞與接收。

Worker: 用於建立單獨的JS執行緒。

worker_threads模組中比較重要的幾個屬性:

parentPort: 子執行緒中的parentPort指向可以與主執行緒進行通訊的MessagePort。

子執行緒向父執行緒傳送訊息

parentPort.postMessage(...)

   

子執行緒接受來自父執行緒的訊息

parentPort.on('message', (msg) => ...)

   

isMainThread: 用於區分當前檔案是否在主執行緒中執行

workerData: 用於傳遞給Worker建構函式的data副本,在子執行緒中可以透過workerData獲取到父程式傳入的資料。

瞭解常用類與屬性之後再來看一下程式碼示例

const { Worker, parentPort, isMainThread } = require('worker_threads');
if (isMainThread) {
  const w = new Worker(__filename, {
    workerData: {
      name: 'Randal'
    }
  });
  w.postMessage(1e10);
  const startTime = Date.now();
  w.on('message', function(msg) {
    console.log('main thread get message: ' + msg);
    console.log('compute time ellapsed: ' + (Date.now() - startTime) / 1000);
  });
  console.log('main thread executing');
} else {
  const longComputation = (val) => {
    let sum = 0;
    for (let i = 0; i  {
    console.log(`${workerData.name} worker get message: ` + msg);
    parentPort.postMessage(longComputation(msg));
  });
}

// 執行結果
main thread executing
Randal worker get message: 10000000000
main thread get message: 49999999990067860000
compute time ellapsed: 14.954

   

執行緒間如何傳輸資料?

port.postMessag(value[, transferList])

   

除了value之外,postMessage方法還支援傳入transferList引數,transferList是一個List,支援的資料型別包括ArrayBuffer和MessagePort物件,transferList中的物件在傳輸完成後,在傳送物件的執行緒中就不可以繼續使用了。

const { Worker, isMainThread, parentPort } = require('worker_threads');
// 主執行緒
if (isMainThread) {
  const sab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100);
  const ia = new Int32Array(sab);

  for (let i = 0; i  {
      console.log('after transfer: ', sab);
    }, 1000);
  }
} else {
  console.log("this isn't main thread");
}
// 輸出結果
this is the main thread
before transfer:  ArrayBuffer { byteLength: 400 }
this isn't main thread
after transfer:  ArrayBuffer { byteLength: 0 }

   

如果ArrayBuffer是透過value傳輸的(且在transferList中不存在),則傳輸過去的是副本,如下所示:

w.postMessage(sab);

// 輸出結果
this is the main thread
before transfer:  ArrayBuffer { byteLength: 400 }
this isn't main thread
after transfer:  ArrayBuffer { byteLength: 400 }

   

執行緒間如何共享記憶體?

輪到SharedArrayBuffer出場了,如果postMessage中的value是SharedArrayBuffer的話,則執行緒之間就可以共享記憶體,如下面例子所示:

const { Worker, isMainThread, parentPort } = require('worker_threads');
// 主執行緒
if (isMainThread) {
  const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 5);
  const ia = new Int32Array(sab);

  for (let i = 0; i  {
    console.log(ia);
   });
  }
} else {
  parentPort.on('message', (msg) => {
    const ia = new Int32Array(msg, 0, 1);
    ia[0] = ia[0] + 1;
    parentPort.postMessage('done');
  });
}

// 輸出結果
Int32Array [ 1, 1, 2, 3, 4 ]
Int32Array [ 2, 1, 2, 3, 4 ]

   

參考資料


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4289/viewspace-2803274/,如需轉載,請註明出處,否則將追究法律責任。

相關文章