怎么做異步計算?下面本篇文章給大家介紹一下利用瀏覽器和 Node.js 的多線程能力做異步計算的方法,希望對大家有所幫助!
都說 Node.js 可以實現(xiàn)高性能的服務器,那什么是高性能呢?
所有的軟件代碼最終都是通過 CPU 來跑的,能不能把 CPU 高效利用起來是區(qū)分性能高低的標志,也就是說不能讓它空轉?!就扑]學習:《nodejs 教程》】
那什么時候會空轉呢?
- 當程序在進行網(wǎng)絡和磁盤的 IO 的時候,這時候 CPU 是空閑的,也就是在空轉。
- 多核 CPU 可以同時跑多個程序,如果只利用了其中一核,那么其他核也是在空轉。
所以,要想達到高性能,就要解決這兩個問題。
操作系統(tǒng)提供了線程的抽象,對應代碼不同的執(zhí)行分支,都是可以同時上不同的 CPU 跑的,這是利用好多核 CPU 性能的方式。
而如果有的線程在進行 IO 了,也就是要阻塞的等待讀寫完成,這種是比較低效的方式,所以操作系統(tǒng)實現(xiàn)了 DMA 的機制,就是設備控制器,由硬件來負責從設備到內存的搬運,在搬完了告訴 CPU 一聲。這樣當有的線程在 IO 的時候就可以把線程暫停掉,等收到 DMA 運輸數(shù)據(jù)完成的通知再繼續(xù)跑。
多線程、DMA,這是利用好多核 CPU 優(yōu)勢、解決 CPU 阻塞等 IO 的問題的操作系統(tǒng)提供的解決方案。
而各種編程語言對這種機制做了封裝,Node.js 也是,Node.js 之所以是高性能,就是因為異步 IO 的設計。
Node.js 的異步 IO 的實現(xiàn)在 libuv,基于操作系統(tǒng)提供的異步的系統(tǒng)調用,這種一般是硬件級別的異步,比如 DMA 搬運數(shù)據(jù)。但是其中有一些同步的系統(tǒng)調用,通過 libuv 封裝以后也會變成異步的,這是因為 libuv 內有個線程池,來執(zhí)行這些任務,把同步的 API 變成異步的。這個線程池的大小可以通過 UV_THREADPOOL_SIZE
的環(huán)境變量設置,默認是 4。
我們在代碼里調用的異步 API,很多都是通過線程來實現(xiàn)的。
比如:
const fsPromises = require('fs').promises; const data = await fsPromises.readFile('./filename');
但是,這種異步 API 只解決了 IO 的問題,那如何利用多核 CPU 的優(yōu)勢來做計算呢?
Node.js 在 10.5 實驗性的引入(在 12 正式引入)了 worker_thread 模塊,可以創(chuàng)建線程,最終用多個 CPU 跑,這是利用多核 CPU 的做計算的方式。
異步 API 可以利用多線程做 IO,而 worker_thread 可以創(chuàng)建線程做計算,用于不同的目的。
要聊清楚 worker_thread,還得從瀏覽器的 web worker 聊起。
瀏覽器的 web worker
瀏覽器也同樣面臨不能利用多核 CPU 做計算的問題,所以 html5 引入了 web worker,可以通過另一個線程做計算。
<!DOCTYPE html> <html> <head></head> <body> <script> (async function () { const res = await runCalcWorker(2, 3, 3, 3); console.log(res); })(); function runCalcWorker(...nums) { return new Promise((resolve, reject) => { const calcWorker = new Worker('./webWorker.js'); calcWorker.postMessage(nums) calcWorker.onmessage = function (msg) { resolve(msg.data); }; calcWorker.onerror = reject; }); } </script> </body> </html>
我們創(chuàng)建一個 Worker 對象,指定跑在另一個線程的 js 代碼,然后通過 postMessage 傳遞消息給它,通過 onMessage 接收消息。這個過程也是異步的,我們進一步把它封裝成了 promise。
然后在 webWorker.js 里面接收數(shù)據(jù),做計算,之后通過 postMessage 傳回結果。
// webWorker.js onmessage = function(msg) { if (Array.isArray(msg.data)) { const res = msg.data.reduce((total, cur) => { return total += cur; }, 0); postMessage(res); } }
這樣,我們就利用了另一個 CPU 核來跑了這段計算,對寫代碼來說和普通的異步代碼沒啥區(qū)別。但這個異步實際上不是 IO 的異步,而是計算的異步。
Node.js 的 worker thread 和 web worker 類似,我甚至懷疑 worker thread 的名字就是受 web worker 影響的。
Node.js 的 worker thread
把上面那段異步計算的邏輯在 Node.js 里面實現(xiàn)話,是這樣的:
const runCalcWorker = require('./runCalcWorker'); (async function () { const res = await runCalcWorker(2, 3, 3, 3); console.log(res); })();
以異步的方式調用,因為異步計算和異步 IO 在使用方式上沒啥區(qū)別。
// runCalcWorker.js const { Worker } = require('worker_threads'); module.exports = function(...nums) { return new Promise(function(resolve, reject) { const calcWorker = new Worker('./nodeWorker.js'); calcWorker.postMessage(nums); calcWorker.on('message', resolve); calcWorker.on('error', reject); }); }
然后異步計算的實現(xiàn)是通過創(chuàng)建 Worker 對象,指定在另一個線程跑的 JS,然后通過 postMessage 傳遞消息,通過 message 接收消息。這個和 web worker 很類似。
// nodeWorker.js const { parentPort } = require('worker_threads'); parentPort.on('message', (data) => { const res = data.reduce((total, cur) => { return total += cur; }, 0); parentPort.postMessage(res); });
在具體執(zhí)行計算的 nodeWorker.js 里面,監(jiān)聽 message 消息,然后進行計算,通過 parentPost.postMessage 傳回數(shù)據(jù)。
對比下 web worker,你會發(fā)現(xiàn)特別的像。所以,我覺得 Node.js 的 worker thread 的 api 是參考 web worker 來設計的。
但是,其實 worker thread 也支持在創(chuàng)建的時候就通過 wokerData 傳遞數(shù)據(jù):
const { Worker } = require('worker_threads'); module.exports = function(...nums) { return new Promise(function(resolve, reject) { const calcWorker = new Worker('./nodeWorker.js', { workerData: nums }); calcWorker.on('message', resolve); calcWorker.on('error', reject); }); }
然后 worker 線程里通過 workerData 來取:
const { parentPort, workerData } = require('worker_threads'); const data = workerData; const res = data.reduce((total, cur) => { return total += cur; }, 0); parentPort.postMessage(res);
因為有個傳遞消息的機制,所以要做序列化和反序列化,像函數(shù)這種無法被序列化的數(shù)據(jù)就無法傳輸了。這也是 worker thread 的特點。
Node.js 的 worker thread 和 瀏覽器 web woker 的對比
從使用上來看,都可以封裝成普通的異步調用,和其他異步 API 用起來沒啥區(qū)別。
都要經過數(shù)據(jù)的序列化反序列化,都支持 postMessage、onMessage 來收發(fā)消息。
除了 message,Node.js 的 worker thread 支持傳遞數(shù)據(jù)的方式