Node.js 工作線程 worker_threads 的使用

在瞭解工作線程的具體用法之前,有必要先想想:工作線程解決了什麼問題?

工作線程主要解決的是cpu密集型場景下的問題,由於node只有單個主線程的特性,導致在執行高cpu運算任務時,會有以下的問題:

  1. 計算任務阻塞主線程,導致無法響應新的請求
  2. 只能單核執行,無法充分利用多核cpu

而工作線程通過開啓在主線程中開啓新的線程單獨執行計算任務,避免了阻塞整個事件循環,使主線程仍然可以繼續處理後續的請求。

並且由於是新的線程,可以在其他cpu核心上執行,使得單個進程可以更充分的利用多核cpu。

用法介紹

生成工作線程

主線程中執行,生成工作線程

const { Worker } = require('worker_threads');
let worker = new Worker('工作線程的js文件路徑', { });
worker.on('message', (val) => {
  resolve(val); //接收工作線程計算完畢後返回的結果
});

線程通信

工作線程中,計算結果,向主線程返回

const { workerData, parentPort } = require('worker_threads');
// workerData 主線程傳來的參數
//計算結果 res
parentPort.postMessage(res); //向主線程返回結果

其他用法見 node文檔

示例

下面是一個阻塞主線程以及使用工作線程優化的示例。示例代碼

使用koa框架啓動了一個簡單的http服務,包含以下三個api

  1. /test :用於測試主線程是否被阻塞,正常情況應當立即返回

  2. /fib:用暴力方式計算斐波那契數,隨着輸入的n增大,運算量呈指數增長,用於測試阻塞主線程

  3. /asyncFib:將計算交由工作線程執行,測試工作線程的優化效果

其中,asyncFib用 Promise 包裝了一下,使得它從事件監聽的形式改成一個普通的異步調用

//app.js
const { Worker } = require('worker_threads');

const Koa = require('koa');
const Router = require('koa-router') 

const app = new Koa();
const router = new Router();
app.use(router.routes());

router.get('/test', async (ctx, next) => {
  ctx.body = 'test';
});

router.get('/fib',async (ctx,next)=>{
  let n = ctx.query.n;
  ctx.body = fib(n);
})

router.get('/asyncFib',async (ctx,next)=>{
  let n = ctx.query.n;
  ctx.body = await asyncFib(n);
})

app.listen(3000);
console.log('http://127.0.0.1:3000');

function fib (n) {
  if (n === 1 || n === 2) return 1;
  return fib(n - 1) + fib(n - 2);
}

async function asyncFib (n) {
  let worker = new Worker('./fib.js', { workerData: n });
  return new Promise((resolve) => {
    worker.on('message', (val) => {
      resolve(val); //接收工作線程計算完畢後返回的結果
    });
  });
}
  • 工作線程運行的代碼
// fib.js
const { workerData, parentPort } = require('worker_threads');
let num = workerData;//獲取參數
let res = fib(num);
parentPort.postMessage(res); //向主線程返回結果

function fib (n) {
  if (n === 1 || n === 2) return 1;
  return fib(n - 1) + fib(n - 2);
}

測試

測試阻塞主線程

  1. 訪問 http://127.0.0.1:3000/test, 立刻返回響應test
  2. 訪問 http://127.0.0.1:3000/fib?n=44,處於pending狀態,在我的測試中約10秒後返回
  3. 再次訪問 http://127.0.0.1:3000/test,也處於peding狀態,要等到/fib請求結束後才能收到響應

可以看到,請求2不僅自己執行的慢,還影響到了後續請求,令其他請求都要等待它執行完成後才能處理。

測試使用工作線程

  1. 訪問 http://127.0.0.1:3000/test, 立刻返回響應test
  2. 訪問 http://127.0.0.1:3000/asyncFib?n=44,處於pending狀態,也是約10秒後返回
  3. 再次訪問 http://127.0.0.1:3000/test,立刻返回響應test

請求2不再阻塞主線程


如何理解

就表現來說,加上了工作線程,請求就突然不被阻塞了。要如何理解這個現象?

首先要明確一點,在優化前後斐波那契數的計算量是固定的,該做的計算並不會憑空消失。工作線程是通過在另一個cpu核心上運行單獨線程進行計算,從而實現不阻塞主線程。

我們都知道node.js 可以非阻塞的執行io操作,可以將工作線程與它進行類比,便於理解。

舉個栗子:假設有這麼一個服務,它以http接口的形式提供了一個計算斐波那契數的api http://api.test.com/fib?n=40 ,再由我們的服務去調用它,這個流程與工作線程執行流程的對照如下:

調用外部的計算服務 使用工作線程
發起http請求,交給io線程,註冊一個回調等待響應

request(url, (res)=>{})
生成工作線程,監聽message事件等待響應

worker.on('message',(res)=>{})
繼續響應其他請求 繼續響應其他請求
遠程的計算服務獲取參數n,用遠程服務器的cpu 計算斐波那契數 工作線程獲取參數n,用本機的其他空閒cpu核心 計算斐波那契數
返回結果,執行回調 返回結果,執行回調

可以看出,除了不阻塞主線程這一個好處之外,工作線程給node.js提供了單進程情況下使用多核cpu的能力,可以承載更多的計算量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章