最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入理解Node.js中的Worker線程

 更新時間:2021年04月29日 08:42:16   作者:淺笑·  
這篇文章主要介紹了深入理解Node.js中的Worker線程,對Worker線程感興趣的同學(xué),一定要看一下

概述

多年以來,Node.js都不是實現(xiàn)高 CPU 密集型應(yīng)用的最佳選擇,這主要就是因為JavaScript的單線程。作為對此問題的解決方案,Node.jsv10.5.0 通過worker_threads模塊引入了實驗性的 “worker 線程” 概念,并從 Node.js v12 LTS 起成為一個穩(wěn)定功能。本文將解釋其如何工作,以及如何使用 Worker 線程獲得最佳性能。

Node.js 中 CPU 密集型應(yīng)用的歷史

在 worker 線程之前,Node.js 中有多種方式執(zhí)行 CPU 密集型應(yīng)用。其中的一些為:

  • 使用child_process模塊并在一個子進程中運行 CPU 密集型代碼
  • 使用cluster模塊,在多個進程中運行多個 CPU 密集型操作
  • 使用諸如 Microsoft 的Napa.js這樣的第三方模塊

但是受限于性能、額外引入的復(fù)雜性、占有率低、薄弱的文檔化等,這些解決方案無一被廣泛采用。

為 CPU 密集型操作使用 worker 線程

盡管對于JavaScript的并發(fā)性問題來說,worker_threads是一個優(yōu)雅的解決方案,但其并未給 JavaScript 本身帶來多線程特性。相反,worker_threads通過運行應(yīng)用使用多個相互隔離的 JavaScript workers 來實現(xiàn)并發(fā),而 workers 和父 worker 之間的通信由 Node 提供。聽懵了嗎? ‍♂️

在 Node.js 中,每一個 worker 將擁有其自己的 V8 實例及事件循環(huán)(Event Loop)。但和child_process不同的是,workers 不共享內(nèi)存。

以上概念會在后面解釋。我們首先來大致看一眼如何使用 Worker 線程。一個原生的用例看起來是這樣的:

// worker-simple.js

const {Worker, isMainThread, parentPort, workerData} = require('worker_threads');
if (isMainThread) {
 const worker = new Worker(__filename, {workerData: {num: 5}});
 worker.once('message', (result) => {
 console.log('square of 5 is :', result);
 })
} else {
 parentPort.postMessage(workerData.num * workerData.num)
}

在上例中,我們向每個單獨的 workder 中傳入了一個數(shù)字以計算其平方值。在計算之后,子 worker 將結(jié)果發(fā)送回主 worker 線程。盡管看上去簡單,但 Node.js 新手可能還是會有點困惑。

Worker 線程是如何工作的?

JavaScript 語言沒有多線程特性。因此,Node.js 的 Worker 線程以一種異于許多其它高級語言傳統(tǒng)多線程的方式行事。

在 Node.js 中,一個 worker 的職責(zé)就是去執(zhí)行一段父 worker 提供的代碼(worker 腳本)。這段 worker 腳本將會在隔絕于其它 workers 的環(huán)境中運行,并能夠在其自身和父 worker 間傳遞消息。worker 腳本既可以是一個獨立的文件,也可以是一段可被eval解析的文本格式的腳本。在我們的例子中,我們將__filename作為 worker 腳本,因為父 worker 和子 worker 代碼都在同一個腳本文件中,由isMainThread屬性決定其角色。

每個 worker 通過message channel連接到其父 worker。子 worker 可以使用parentPort.postMessage()函數(shù)向消息通道中寫入信息,父 worker 則通過調(diào)用 worker 實例上的worker.postMessage()函數(shù)向消息通道中寫入信息??匆幌聢D 1:

一個 Message Channel 就是一個簡單的通信渠道,其兩端被稱作 ‘ports'。在 JavaScript/NodeJS 術(shù)語中,一個 Message Channel 的兩端就被叫做port1和port2

Node.js 的 workers 是如何并行的?

現(xiàn)在關(guān)鍵的問題來了,JavaScript 并不直接提供并發(fā),那么兩個 Node.js workers 要如何并行呢?答案就是V8 isolate。

一個V8 isolate就是 chrome V8 runtime 的一個單獨實例,包含自有的 JS 堆和一個微任務(wù)隊列。這允許了每個 Node.js worker 完全隔離于其它 workers 地運行其 JavaScript 代碼。其缺點在于 worker 無法直接訪問其它 workers 的堆數(shù)據(jù)了。

擴展閱讀:JS在瀏覽器和Node下是如何工作的?

由此,每個 worker 將擁有其自己的一份獨立于父 worker 和其它 workers 的 libuv 事件循環(huán)的拷貝。

跨越 JS/C++ 的邊界

實例化一個新 worker、提供和父級/同級 JS 腳本的通信,都是由 C++ 實現(xiàn)版本的 worker 完成的。在成文時,該實現(xiàn)為worker.cc(https://github.com/nodejs/node/blob/921493e228/src/node_worker.cc)。

Worker 的實現(xiàn)通過worker_threads模塊被暴露為用戶級的 JavaScript 腳本。該 JS 實現(xiàn)被分割為兩個腳本,我將之稱為:

  • 初始化腳本 worker.js— 負責(zé)初始化 worker 實例,并建立初次父子 worker 通信,以確保從父 worker 傳遞 worker 元數(shù)據(jù)至子 worker。(https://github.com/nodejs/node/blob/921493e228/lib/internal/worker.js)
  • 執(zhí)行腳本 worker_thread.js— 根據(jù)用戶提供的workerData數(shù)據(jù)和其它父 worker 提供的元數(shù)據(jù)執(zhí)行用戶的 worker JS 腳本。(https://github.com/nodejs/node/blob/921493e228/lib/internal/main/worker_thread.js)

圖 2 以更清晰的方式解釋了這個過程:

基于上述,我們可以將 worker 設(shè)置過程劃分為兩個階段:

  • worker 初始化
  • 運行 worker

來看看每個階段都發(fā)生了什么吧:

初始化步驟

1.用戶級腳本通過使用worker_threads創(chuàng)建一個 worker 實例

2.Node 的父 worker 初始化腳本調(diào)用 C++ 并創(chuàng)建一個空的 worker 對象。此時,被創(chuàng)建的 worker 還只是個未被啟動的簡單的 C++ 對象

3.當(dāng) C++ worker 對象被創(chuàng)建后,其生成一個線程 ID 并賦值給自身

4.同時,一個空的初始化消息通道(讓我們稱之為IMC)被父 worker 創(chuàng)建。圖 2 中灰色的 “Initialisation Message Channel” 部分展示了這點

5.一個公開的 JS 消息通道(稱其為PMC)被 worker 初始化腳本創(chuàng)建。該通道被用戶級 JS 使用以在父子 worker 之間傳遞消息。圖 1 中主要描述了這部分,也在圖 2 中被標為了紅色。

6.Node 父 worker 初始化腳本調(diào)用 C++ 并將需要被發(fā)送到 worker 執(zhí)行腳本中的初始元數(shù)據(jù)寫入IMC。

什么是初始元數(shù)據(jù)?即執(zhí)行腳本需要了解以啟動 worker 的數(shù)據(jù),包括腳本名稱、worker 數(shù)據(jù)、PMC 的port2,以及其它一些信息。

按我們的例子來說,初始化元數(shù)據(jù)如:

:phone: 嘿!worker 執(zhí)行腳本,請你用{num: 5}這樣的 worker 數(shù)據(jù)運行一下worker-simple.js好嗎?也請你把 PMC 的port2傳遞給它,這樣 worker 就能從 PMC 讀取數(shù)據(jù)啦。

下面的小片段展示了初始化數(shù)據(jù)如何被寫入 IMC:

const kPublicPort = Symbol('kPublicPort');
// ...

const { port1, port2 } = new MessageChannel();
this[kPublicPort] = port1;
this[kPublicPort].on('message', (message) => this.emit('message', message));
// ...

this[kPort].postMessage({
  type: 'loadScript',
  filename,
  doEval: !!options.eval,
  cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
  workerData: options.workerData,
  publicPort: port2,
  // ...
  hasStdin: !!options.stdin
}, [port2]);

代碼中的this[kPort]是初始化腳本中 IMC 的端點。盡管 worker 初始化腳本向 IMC 寫入了數(shù)據(jù),但 worker 執(zhí)行腳本仍無法訪問該數(shù)據(jù)。

運行步驟

此時,初始化已告一段落;接下來 worker 初始化腳本調(diào)用 C++ 并啟動 worker 線程。

1.一個新的V8 isolate被創(chuàng)建并被分配給 worker。前面講過,一個 “v8 isolate” 就是 chrome V8 runtime 的一個單獨實例。這使得 worker 線程的執(zhí)行上下文隔離于應(yīng)用代碼中的其它部分。

2.libuv被初始化。這確保了 worker 線程保有其自己獨立于應(yīng)用中的其它部分事件循環(huán)。

3.worker 執(zhí)行腳本被執(zhí)行,并且 worker 的事件循環(huán)被啟動。

4.worker 執(zhí)行腳本調(diào)用 C++ 并從 IMC 中讀取初始化元數(shù)據(jù)。

5.worker 執(zhí)行腳本執(zhí)行對應(yīng)文件或代碼(在我們的例子中就是worker-simple.js),以作為一個 worker 開始運行。

看看下面的代碼片段,worker 執(zhí)行腳本是如何從 IMC 讀取數(shù)據(jù)的:

const publicWorker = require('worker_threads');

// ...

port.on('message', (message) => {
  if (message.type === 'loadScript') {
    const {
      cwdCounter,
      filename,
      doEval,
      workerData,
      publicPort,
      manifestSrc,
      manifestURL,
      hasStdin
    } = message;

    // ...
    initializeCJSLoader();
    initializeESMLoader();
    
    publicWorker.parentPort = publicPort;
    publicWorker.workerData = workerData;

    // ...
    
    port.unref();
    port.postMessage({ type: UP_AND_RUNNING });
    if (doEval) {
      const { evalScript } = require('internal/process/execution');
      evalScript('[worker eval]', filename);
    } else {
      process.argv[1] = filename; // script filename
      require('module').runMain();
    }
  }
  // ...

是否注意到以上片段中的workerData和parentPort屬性被指定給了publicWorker對象呢?后者是在 worker 執(zhí)行腳本中由require('worker_threads')引入的。

這就是為何workerData和parentPort屬性只在子 worker 線程內(nèi)部可用,而在父 worker 的代碼中不可用了。

如果嘗試在父 worker 代碼中訪問這兩個屬性,都會返回null。

充分利用 worker 線程

現(xiàn)在我們理解 Node.js 的 worker 線程是如何工作的了,這的確能幫助我們在使用 Worker 線程時獲得最佳性能。當(dāng)編寫比worker-simple.js更復(fù)雜的應(yīng)用時,需要記住以下兩個主要的關(guān)注點:

盡管 worker 線程比真正的進程更輕量,但如果頻繁讓 workers 陷入某些繁重的工作仍會開銷巨大。

使用 worker 線程承擔(dān)并行 I/O 操作仍是不劃算的,因為 Node.js 原生的 I/O 機制是比從頭啟動一個 worker 線程去做同樣的事更快的方式。

為了克服第 1 點的問題,我們需要實現(xiàn)“worker 線程池”。

worker 線程池

Node.js 的 worker 線程池是一組正在運行且能夠被后續(xù)任務(wù)利用的 worker 線程。當(dāng)一個新任務(wù)到來時,它可以通過父子消息通道被傳遞給一個可用的 worker。一旦完成了這個任務(wù),子 worker 能將結(jié)果通過同樣的消息通道回傳給父 worker。

一旦實現(xiàn)得當(dāng),由于減少了創(chuàng)建新線程帶來的額外開銷,線程池可以顯著改善性能。同樣值得一提的是,因為可被有效運行的并行線程數(shù)總是受限于硬件,創(chuàng)建一堆數(shù)目巨大的線程同樣難以奏效。

下圖是對三臺 Node.js 服務(wù)器的一個性能比較,它們都接收一個字符串并返回做了 12 輪加鹽處理的一個 Bcrypt 哈希值。三臺服務(wù)器分別是:

  • 不用多線程
  • 多線程,沒有線程池
  • 有 4 個線程的線程池

一眼就能看出,隨著負載增長,使用一個線程池擁有顯著小的開銷。

但是,截止成文之時,線程池仍不是 Node.js 開箱即用的原生功能。因此,你還得依賴第三方實現(xiàn)或編寫自己的 worker 池。

希望你現(xiàn)在能深入理解了 worker 線程如何工作,并能開始體驗并利用 worker 線程編寫你的 CPU 密集型應(yīng)用。

以上就是深入理解Node.js中的Worker線程的詳細內(nèi)容,更多關(guān)于Node.js的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • nodejs中使用throw進行異常處理的操作方法

    nodejs中使用throw進行異常處理的操作方法

    最近在寫一個nodejs項目時在思考一個問題: 在方法調(diào)其他方法的過程中,其他方法是采用return來返回特定格式的錯誤數(shù)據(jù)還是使用throw來直接拋出錯誤?所以本文給大家介紹了nodejs中使用throw進行異常處理的操作方法,需要的朋友可以參考下
    2024-12-12
  • 詳解用node.js實現(xiàn)簡單的反向代理

    詳解用node.js實現(xiàn)簡單的反向代理

    本篇文章主要介紹了詳解用node.js實現(xiàn)簡單的反向代理,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-06-06
  • 詳解Node中導(dǎo)入模塊require和import的區(qū)別

    詳解Node中導(dǎo)入模塊require和import的區(qū)別

    本篇文章主要介紹了詳解Node中導(dǎo)入模塊require和import的區(qū)別,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-08-08
  • 詳解如何利用Nodejs構(gòu)建多進程應(yīng)用

    詳解如何利用Nodejs構(gòu)建多進程應(yīng)用

    這篇文章主要為大家介紹了如何利用Nodejs構(gòu)建多進程應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • nodejs+mongodb aggregate級聯(lián)查詢操作示例

    nodejs+mongodb aggregate級聯(lián)查詢操作示例

    這篇文章主要介紹了nodejs+mongodb aggregate級聯(lián)查詢操作,結(jié)合實例形式分析了基于nodejs的mongodb數(shù)據(jù)庫級聯(lián)查詢相關(guān)操作技巧,需要的朋友可以參考下
    2018-03-03
  • NodeJS GRPC簡單的示例詳解

    NodeJS GRPC簡單的示例詳解

    本文介紹了如何使用NodeJS GRPC,包括.proto文件的定義、客戶端和服務(wù)器的實現(xiàn),首先,創(chuàng)建.proto文件定義服務(wù)和消息,然后實現(xiàn)客戶端greeter_client.js文件,最后運行服務(wù)器和客戶端,通過這篇文章,你可以快速掌握NodeJS GRPC的基礎(chǔ)使用方法
    2024-10-10
  • 使用nvm管理多版本node的詳細教程

    使用nvm管理多版本node的詳細教程

    在開發(fā)工作中,經(jīng)常在不同的項目中使用不同版本的node去開發(fā),換一個項目在重新安裝node太麻煩,所以使用nvm來管理多版本的node開發(fā)環(huán)境,就非常方便了,所以本文給大家介紹了如何使用nvm管理多版本node,需要的朋友可以參考下
    2024-08-08
  • Nodejs 中的 Buffer 類的創(chuàng)建與基本使用

    Nodejs 中的 Buffer 類的創(chuàng)建與基本使用

    這篇文章主要為大家介紹了Nodejs中Buffer的使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • node.js中的fs.readFile方法使用說明

    node.js中的fs.readFile方法使用說明

    這篇文章主要介紹了node.js中的fs.readFile方法使用說明,本文介紹了fs.readFile的方法說明、語法、接收參數(shù)、使用實例和實現(xiàn)源碼,需要的朋友可以參考下
    2014-12-12
  • 傻瓜式解讀koa中間件處理模塊koa-compose的使用

    傻瓜式解讀koa中間件處理模塊koa-compose的使用

    這篇文章主要介紹了傻瓜式解讀koa中間件處理模塊koa-compose的使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-10-10

最新評論

云霄县| 白山市| 博野县| 清丰县| 临桂县| 惠东县| 奉新县| 平顺县| 彭州市| 慈溪市| 南宫市| 西昌市| 昌邑市| 科尔| 大方县| 浏阳市| 柞水县| 望奎县| 大余县| 札达县| 洛浦县| 绥芬河市| 寿宁县| 浦县| 钟山县| 永川市| 峨眉山市| 侯马市| 中超| 石景山区| 承德县| 丰台区| 广河县| 揭西县| 河南省| 北流市| 东莞市| 安溪县| 同德县| 宁乡县| 东光县|