在瞭解工作線程的具體用法之前,有必要先想想:工作線程解決了什麼問題?
工作線程主要解決的是cpu密集型場景下的問題,由於node只有單個主線程的特性,導致在執行高cpu運算任務時,會有以下的問題:
- 計算任務阻塞主線程,導致無法響應新的請求
- 只能單核執行,無法充分利用多核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
-
/test
:用於測試主線程是否被阻塞,正常情況應當立即返回 -
/fib
:用暴力方式計算斐波那契數,隨着輸入的n增大,運算量呈指數增長,用於測試阻塞主線程 -
/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);
}
測試
測試阻塞主線程
- 訪問
http://127.0.0.1:3000/test
, 立刻返回響應test - 訪問
http://127.0.0.1:3000/fib?n=44
,處於pending狀態,在我的測試中約10秒後返回 - 再次訪問
http://127.0.0.1:3000/test
,也處於peding狀態,要等到/fib
請求結束後才能收到響應
可以看到,請求2不僅自己執行的慢,還影響到了後續請求,令其他請求都要等待它執行完成後才能處理。
測試使用工作線程
- 訪問
http://127.0.0.1:3000/test
, 立刻返回響應test - 訪問
http://127.0.0.1:3000/asyncFib?n=44
,處於pending狀態,也是約10秒後返回 - 再次訪問
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的能力,可以承載更多的計算量。