node.js與多線程技術之worker threads

原文java

對於想了解,進程,線程,io這些東西的朋友推薦個文章node

想要明白workers,首先須要明白node是怎樣構成的。
當一個node進程開始,它實際上是:chrome

  1. 一個進程。
  2. 一個線程。
  3. 一個事件輪垂。
  4. 一個js引擎實例。
  5. 一個node.js實例。

一個進程:是指一個全局對象,這個對象可以訪問任何地方,而且包含當前處理時的此時信息。json

一個線程:單線程意味着單位時間內只有一組指令在給定的進程中執行。api

一個事件輪垂:這是理解Node最重要的概念。它使Node更夠異步以及擁有無鎖定I/O。即便js是單線程的,經過提供一些系統核心的操做像是回調函數,promise函數以及異步的async/await函數這些功能。promise

一個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。

const worker = 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數據的的複製數據。

在實戰中,上面的任務最好使用線程池來替代。不然,開銷可能大於好處。

對Worker的指望是什麼(但願是):

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

對Worker不指望的是:

  • 不要認爲worker會使全部的東西都很快速,有些狀況下最好使用線程池。
  • 不要使用worker來進行io並行操做。
  • 不要認爲衍生一個線程成本很低。

最後:

Workers有chrome開發工具,可用來監視Node.js中的workers。

相關文章
相關標籤/搜索