1. 程式人生 > 實用技巧 >Node.js中的Worker Threads

Node.js中的Worker Threads

想要明白workers,首先需要明白node是怎樣構成的。當一個node程序開始,它其實是:

  1. 一個程序。
  2. 一個執行緒。
  3. 一個事件輪垂。
  4. 一個js引擎例項。
  5. 一個node.js例項。

一個程序:是指一個全域性物件,這個物件能夠訪問任何地方,並且包含當前處理時的此時資訊。

一個執行緒:單執行緒意味著單位時間內只有一組指令在給定的程序中執行。

一個事件輪垂:這是理解Node最重要的概念。它使Node更夠非同步以及擁有無鎖定I/O。即使js是單執行緒的,通過提供一些系統核心的操作像是回撥函式,promise函式以及非同步的async/await函式這些功能。

一個JS引擎例項:這是個計算機程式,用來執行js的程式碼。

一個Node.js例項:一個計算機程式用來執行node.js的程式碼。

一句話,Node執行在一個單執行緒上,每次事件輪垂只有一個程序存在。一個程式碼一次執行(不是並行執行)。這個非常重要,因為它很簡單,你不用考慮併發的問題。

這麼設計的原因是因為js生出來最初是用來開發客戶端互動的(像是頁面互動,表單這些),沒有對執行緒這種用複雜的需求。

但是,和所有的事情一樣,這樣也有缺點:如果你有cpu敏感的程式碼,例如記憶體中有大量的來回計算的複雜資料,那麼這能鎖住其他需要進行處理計算的任務。像是,你向伺服器發起一個請求,應對這個請求的介面有cpu敏感的程式碼,那麼它就能鎖定事件輪垂進而阻止其他請求的處理(筆者:其實就是其他請求就需要長時間排隊甚至超時)。

如果主事件輪垂必須等待一個函式執行完成然後才能執行其他命令,那麼這個函式就是“鎖定中”。一個無鎖定函式會允許主事件輪垂從開始就持續地執行,並且在其執行完成時通知主事件輪垂呼叫回撥函式。

黃金準則:不要鎖定事件輪垂,儘量關注和避免那些可能造成鎖定的任務,像是同步網路呼叫或者無線死迴圈。

明白cpu操作和i/o操作是很重要的。如上所講,Node中的程式碼不能並行執行。只是i/o是並行,因為他們是非同步執行的。

所以,worker執行緒(以下我們會使用這個node特有的概念)不能提升多少i/o敏感的任務,因為非同步i/o本身就比worker高效很多。worker的主要任務是提升cpu敏感操作的效能。

已有的解決方案

此外,這裡已經有一些應對cpu敏感處理的方案:多程序(例如,cluster API)來保證cpu最大被利用。

這個方法好處是允許每個程序間是獨立的,如果某個執行緒出了問題,不會影響到其他的。他們穩定且相同的api。然而,這意味著犧牲了記憶體共享,並且資料通訊必須用json(有額外開銷,效能稍低)。

JavaScript和Node.js是永遠不會有多執行緒的。原因如下:

so,有人或許會考慮給node.js新增一個新的模組來允許我們建立一個同步執行緒,以此來解決cpu敏感處理的問題。

然而,這不會實現的。如果新增一個執行緒,這個語言的本質就會發生變化。使用類或者函式新增一個執行緒作為新特性是不可能。在支援多執行緒的語言中(如java),“synchronized”之類的關鍵字就能幫助實現多執行緒。

還有,一些資料不是原子的,意味著如果你不是同步處理他們,你可能的結果是在兩個執行緒上都可以訪問並更改這個值得變數,最後得到一個兩個執行緒都對這個者進行了一些改變的無效的值。例如一個簡單的0.1+20.2的操作,這個操作擁有17為小數。

因為小數點不是100%準確的,所以如果不是同步的,有一個整數可能使用worker之後得到一個非整數的數字。

最好的解決方案是

提高cpu效能的最好的方案是使用worker執行緒。瀏覽器很早既有了worker這個概念了。

使億有的結構從:

一個程序

一個執行緒

一個事件輪垂

一個JS隱情例項

一個Node.js例項

變成:

一個程序

多個執行緒

每個執行緒一個事件輪垂

每個執行緒一個JS隱情例項

每個執行緒一個Node.js例項

worker_threads模組能夠實現使用執行緒實現並行執行js。

constworker =require('worker_threads');

Worker Theads在Node.10時開始可以使用,但是一直處於實驗狀態,在12.11.0時,變成穩定版本。

這個方案的意思是,在一個程序中擁有多個Node.js的例項。在worker threads中,一個執行緒可以有一些節點,這個節點不必是父程序。worker結束後還被分配著一些資源不是好的實踐,這會導致記憶體洩漏。我們想把node.js整個的潛入其中,並且給與Node.js去建立新的現成的能力,然後線上程中建立一個新的Node.js例項。本質上是獨立執行在一個程序中的執行緒中。

下面這些使Worker Theads與眾不同:

ArrayBuffers線上程間傳遞記憶體。
SharedArrayBuffer每個執行緒都可訪問,線上程間分享記憶體。(只限二進位制資料)。
Atomics已可用,允許你並行執行一些處理,更高效且允許你在js中實現條件變數。
MessagePort,用來在不同執行緒間進行通訊。可以用來傳遞結構資料,記憶體域,以及不同Worker之間的MessagePort(物件)。
MessageChannel代表一個非同步的,雙向通訊的頻道,用來在不同的(worker)執行緒間通訊。
WorkerData用來傳遞起始資料。任意js資料的複製版本會被傳遞到這個Worker的建構函式中。如果使用postMessage(),資料也會被複制。

介面API

  • const{worker, parentPort} = require('worker_threads'),worker類表示一個獨立執行js的執行緒,parentPort是一個message port的例項。
  • new Worker(filename)或者new worker(code,{eval:true})兩種開始一個worker的方法。(傳遞一個檔名字或需要執行的程式碼)。建議在生產中使用檔名字。
  • worker.on('message'),worker.postmessage(data)`監聽資訊以及在不同的執行緒間釋出資料。
  • parentPort.on('message'),parentPort.postMessage(data),使用parentPort.postMessage()傳送資訊,在父執行緒中使用worker.on('message')來獲取。在父執行緒中使用worker.postMessage()在該執行緒中(當前執行緒是子)使用parentPort.on('message')類獲取。

示例

const { Worker } = require('worker_threads');

const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.once('message',
    message => parentPort.postMessage({ pong: message }));  
`, { eval: true });
worker.on('message', message => console.log(message));      
worker.postMessage('ping');  

執行:

$ node --experimental-worker test.js
{ pong: ‘ping’ }

這段程式碼實際做的是使用new Worker建立了一個執行緒,線上程的內部使用parentPort來監聽和接受一次性的message資訊,接收到資訊後也會發佈一個message個豬執行緒。

在只支援實驗性worker thread的node版本中你必須使用--experimental-worker命令列選項來執行程式碼。

其他例子:

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', resolve);
          worker.on('error', reject);
          worker.on('exit', (code) => {
            if (code !== 0)
              reject(new Error(`Worker stopped with exit code ${code}`));
          });
        });
      };
    } else {
      const { parse } = require('some-js-parsing-library');
      const script = workerData;
      parentPort.postMessage(parse(script));
    }

需要依賴:
Worker該類代表一個獨立的js執行執行緒。
isMainThead一個布林值,當前程式碼是否執行在Worker執行緒中。
parentPortMessagePort物件,如果當前執行緒是個生成的Worker執行緒,則允許和父執行緒通訊。
workerData一個可以傳遞給執行緒建構函式的任何js資料的的複製資料。

在實戰中,上面的任務最好使用執行緒池來替代。否則,開銷可能大於好處。

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

對Worker的期望是什麼(希望是):

  • 傳遞本地處理任務。(passing native handles around)
  • 鎖死檢測。鎖死是指一種情形,一系列程序被鎖定,因為每個程序都把持了一些資源,而且每個執行緒又在等待其他執行緒所把持的資源釋放然後獲取。鎖死檢測在worker thead中比較有用。
  • 更多的隔離,所以一旦一個執行緒收到了影響,其他的沒事。

對Worker不期望的是:

  • 不要認為worker會使所有的東西都很快速,有些情況下最好使用執行緒池。
  • 不要使用worker來進行io並行操作。
  • 不要認為衍生一個執行緒成本很低。