首頁  >  文章  >  web前端  >  聊聊利用Node.js 的多執行緒能力怎麼做非同步計算

聊聊利用Node.js 的多執行緒能力怎麼做非同步計算

青灯夜游
青灯夜游轉載
2021-09-03 18:09:161904瀏覽

怎麼做非同步計算?以下這篇文章為大家介紹一下利用瀏覽器和 Node.js 的多執行緒能力做非同步運算的方法,希望對大家有幫助!

聊聊利用Node.js 的多執行緒能力怎麼做非同步計算

都說 Node.js 可以實現高效能的伺服器,那什麼是高效能呢?

所有的軟體程式碼最終都是透過 CPU 來跑的,能不能把 CPU 高效利用起來是區分效能高低的標誌,也就是說不能讓它空轉。 【推薦學習:《nodejs 教學》】

那什麼時候會空轉呢?

  • 當程式在進行網路和磁碟的 IO 的時候,這時候 CPU 是空閒的,也就是在空轉。
  • 多核心 CPU 可以同時執行多個程序,如果只利用了其中一核,那麼其他核心也是在空轉。

所以,要達到高效能,就要解決這兩個問題。

作業系統提供了執行緒的抽象,對應程式碼不同的執行分支,都是可以同時上不同的 CPU 跑的,這是利用好多核心 CPU 效能的方式。

而如果有的執行緒在進行IO 了,也就是要阻塞的等待讀寫完成,這種是比較低效的方式,所以作業系統實作了DMA 的機制,就是裝置控制器,由硬體來負責從設備到記憶體的搬運,在搬完了告訴CPU 一聲。這樣當有的線程在 IO 的時候就可以把線程暫停掉,等收到 DMA 運輸資料完成的通知再繼續跑。

多執行緒、DMA,這是利用好多核心 CPU 優勢、解決 CPU 阻塞等 IO 的問題的作業系統所提供的解決方案。

而各種程式語言對這種機製做了封裝,Node.js 也是,Node.js 之所以是高效能,就是因為非同步 IO 的設計。

Node.js 的非同步 IO 的實作在 libuv,基於作業系統提供的非同步的系統調用,這種一般是硬體層級的非同步,例如 DMA 搬運資料。但其中有一些同步的系統調用,透過 libuv 封裝以後也會變成異步的,這是因為 libuv 內有個線程池,來執行這些任務,把同步的 API 變成異步的。這個線程池的大小可以透過 UV_THREADPOOL_SIZE 的環境變數設置,預設是 4。

聊聊利用Node.js 的多執行緒能力怎麼做非同步計算

我們在程式碼裡呼叫的非同步 API,很多都是透過執行緒來實現的。

例如:

const fsPromises = require('fs').promises;

const data = await fsPromises.readFile('./filename');

但是,這種非同步 API 只解決了 IO 的問題,那如何利用多核心 CPU 的優勢來做計算呢?

Node.js 在 10.5 實驗性的引入(在 12 正式引入)了 worker_thread 模組,可以創建線程,最終用多個 CPU 跑,這是利用多核心 CPU 的做計算的方式。

非同步 API 可以利用多執行緒做 IO,而 worker_thread 可以建立執行緒做計算,用於不同的目的。

要聊清楚 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(&#39;./webWorker.js&#39;);
                calcWorker.postMessage(nums)
                calcWorker.onmessage = function (msg) {
                    resolve(msg.data);
                };
                calcWorker.onerror = reject;
            });
        }
    </script>

</body>
</html>

我們建立一個 Worker 對象,指定跑在另一個執行緒的 js 程式碼,然後透過 postMessage 傳遞訊息給它,透過 onMessage 接收訊息。這個過程也是異步的,我們進一步把它封裝成了 promise。

然後在 webWorker.js 裡面接收數據,做計算,之後透過 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 核來跑了這段計算,對寫程式碼來說和普通的非同步程式碼沒啥區別。但這個非同步其實不是 IO 的非同步,而是計算的非同步。

Node.js 的 worker thread 和 web worker 類似,我甚至懷疑 worker thread 的名字就是受 web worker 影響的。

Node.js 的worker thread

把上面那段非同步計算的邏輯在Node.js 裡面實作話,是這樣的:

const runCalcWorker = require(&#39;./runCalcWorker&#39;);

(async function () {
    const res = await runCalcWorker(2, 3, 3, 3);
    console.log(res);
})();

以非同步的方式調用,因為非同步計算和非同步IO 在使用方式上沒啥區別。

// runCalcWorker.js
const  { Worker } = require(&#39;worker_threads&#39;);

module.exports = function(...nums) {
    return new Promise(function(resolve, reject) {
        const calcWorker = new Worker(&#39;./nodeWorker.js&#39;);
        calcWorker.postMessage(nums);

        calcWorker.on(&#39;message&#39;, resolve);
        calcWorker.on(&#39;error&#39;, reject);
    });
}

然後非同步計算的實作是透過建立 Worker 對象,指定在另一個執行緒跑的 JS,然後透過 postMessage 傳遞訊息,透過 message 接收訊息。這個和 web worker 很類似。

// nodeWorker.js
const {
    parentPort
} = require(&#39;worker_threads&#39;);

parentPort.on(&#39;message&#39;, (data) => {
    const res = data.reduce((total, cur) => {
        return total += cur;
    }, 0);
    parentPort.postMessage(res);
});

在具體執行計算的 nodeWorker.js 裡面,監聽 message 訊息,然後進行計算,透過  parentPost.postMessage 傳回資料。

對比下 web worker,你會發現特別的像。所以,我覺得 Node.js 的 worker thread 的 api 是參考 web worker 來設計的。

但是,其實 worker thread 也支援在創建的時候就透過 wokerData 傳遞資料:

const  { Worker } = require(&#39;worker_threads&#39;);

module.exports = function(...nums) {
    return new Promise(function(resolve, reject) {
        const calcWorker = new Worker(&#39;./nodeWorker.js&#39;, {
            workerData: nums
        });
        calcWorker.on(&#39;message&#39;, resolve);
        calcWorker.on(&#39;error&#39;, reject);
    });
}

然後 worker 執行緒裡透過 workerData 來取:

const {
    parentPort,
    workerData
} = require(&#39;worker_threads&#39;);

const data = workerData;
const res = data.reduce((total, cur) => {
    return total += cur;
}, 0);
parentPort.postMessage(res);

因为有个传递消息的机制,所以要做序列化和反序列化,像函数这种无法被序列化的数据就无法传输了。这也是 worker thread 的特点。

Node.js 的 worker thread 和 浏览器 web woker 的对比

从使用上来看,都可以封装成普通的异步调用,和其他异步 API 用起来没啥区别。

都要经过数据的序列化反序列化,都支持 postMessage、onMessage 来收发消息。

除了 message,Node.js 的 worker thread 支持传递数据的方式更多,比如还有 workerData。

但从本质上来看,两者都是为了实现异步计算,充分利用多核 CPU 的性能,没啥区别。

总结

高性能的程序也就是要充分利用 CPU 资源,不要让它空转,也就是 IO 的时候不要让 CPU 等,多核 CPU 也要能同时利用起来做计算。操作系统提供了线程、DMA的机制来解决这种问题。Node.js 也做了相应的封装,也就是 libuv 实现的异步 IO 的 api,但是计算的异步是 Node 12 才正式引入的,也就是 worker thread,api 设计参考了浏览器的 web worker,传递消息通过 postMessage、onMessage,需要做数据的序列化,所以函数是没法传递的。

从使用上来看异步计算、异步 IO 使用方式一样,但是异步 IO 只是让 cpu 不同阻塞的等待 IO 完成,异步计算是利用了多核 CPU 同时进行并行的计算,数倍提升计算性能。

更多编程相关知识,请访问:编程视频!!

以上是聊聊利用Node.js 的多執行緒能力怎麼做非同步計算的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除