首頁  >  文章  >  web前端  >  深入了解Node.js的中Worker Threads(工作執行緒)

深入了解Node.js的中Worker Threads(工作執行緒)

青灯夜游
青灯夜游轉載
2021-06-28 11:25:402988瀏覽

深入了解Node.js的中Worker Threads(工作執行緒)

【推薦學習:《nodejs 教學》】

理解 Node 的底層對於理解 Workers 是很有必要的。

當一個Node.js 的應用程式啟動的同時,它會啟動如下模組:

  • 一個行程
  • #一個執行緒
  • #事件循環機制
  • JS 引擎實例
  • Node.js 實例

#一個進程:process 物件是一個全域變量,可在Node.js 程式中任意地方訪問,並提供當前進程的相關資訊。

一個執行緒:單執行緒意味著在目前行程中同一時刻只有一個指令在執行。

事件循環:這是Node.js 中需要重點理解的一個部分,儘管JavaScript 是單線程的,但透過使用回調,promises, async/await 等語法,基於事件循環將對作業系統的操作非同步化,使得Node 擁有非同步非阻塞IO 的特性。

一個 JS 引擎實例:也就是一個可以執行 JavaScript 程式碼的程式。

一個 Node.js 實例:即一個可以執行 Node.js 環境的程式。

換言之,Node 運行在單線程上,並且在事件循環中同一時刻只有一個進程的任務被執行,每次同一時刻只會執行一段代碼(多段代碼不會同時執行)。這是非常有效的,因為這樣的機制夠簡單,讓你在使用 JavaScript 的時候無需擔心並發程式設計的問題。

這樣的原因在於 JavaScript 起初是用於客戶端的互動(例如 web 頁面的互動或表單的驗證),這些邏輯並不需要多執行緒這樣的機制來處理。

所以這也帶來了另一個缺點:如果你需要使用 CPU 密集型的任務,例如在記憶體中使用一個大的資料集進行複雜計算,它會阻塞掉其他進程的任務。同樣的,當你在發起一個有 CPU 密集型任務的遠端介面請求時,也同樣會阻塞掉其他需要被執行的請求。

如果一個函數阻塞了事件循環機制直到這個函數執行完才能執行下一個函數,那麼它就被認為是一個阻塞型函數。一個非阻塞的函數是不會阻塞住事件循環進行下一個函數的執行的,它會使用回呼通知事件循環函數任務已執行完畢。

最佳實務:不要阻塞事件循環,要讓事件循環保持不斷運行,並且注意避免使用回阻塞執行緒的操作例如同步的網路介面呼叫或死循環。

區分開 CPU 密集型操作與 I/O(input/output) 密集型操作是很重要的。像前面所說的,Node.js 並不會同時執行多段程式碼,只有 I/O 操作才會同時去執行,因為它們是異步的。

所以Worker Threads 對於I/O 密集型操作是沒有太大的幫助的,因為非同步的I/O 操作比worker 更有效率,Wokers 的主要作用是用於提升對於CPU 密集型操作的性能。

其他方案

此外,目前已經存在很多對於CPU 密集型操作的解決方案,例如多進程(cluster API)方案,保證了充分利用多核心CPU 。

這個方案的好處在於進程之間是相互獨立的,如果一個進程出現了問題,並不會影響到其他進程。此外它們還擁有穩定的 API,然而,這也意味著不能同享記憶體空間,而且進程間通訊只能透過 JSON 格式的資料進行互動。

JavaScript 和Node.js 不會有多線程,理由如下:

所以,人們可能會認為添加一個創建和同步線程的Node.js 核心模組就可以解決CPU 密集型操作的需求。

然而並不是,如果加入多執行緒模組,將會改變語言本身的特性。添加多線程模組作為可用的類別或函數是不可能的。在一些支援多執行緒的語言例如 Java 中,使用同步特性來使得多個執行緒之間的同步能夠實現。

並且有些數字類型是不夠原子性的,這意味著如果你不同步操作它們,在多執行緒的同時執行計算的情況下,變數的值可能會不斷變動,沒有確定的值,變數的值可能經過一個執行緒計算後改變了幾個字節,在另一個執行緒計算後有改變了其他幾個位元組的資料。例如,在 JavaScript 中一些簡單的計算像 0.1 0.2 的結果中小數部分有 17 位(小數的最高位數)。

var x = 0.1 + 0.2; // x will be 0.30000000000000004

但是浮點數的計算並不是 100% 精準的。所以如果不同步計算,小數部分的數字就會因為多個執行緒永遠沒有一個準確的數字。

最佳實踐

所以解決 CPU 密集型操作的效能問題是使用 Worker Threads。瀏覽器在很久以前就已經有了 Workers 特性了。

單執行緒下的 Node.js:

  • 一个进程
  • 一个线程
  • 一个事件循环
  • 一个 JS 引擎实例
  • 一个 Node.js 实例

多线程 Workers 下 Node.js 拥有:

  • 一个进程
  • 多个线程
  • 每个线程都拥有独立的事件循环
  • 每个线程都拥有一个 JS 引擎实例
  • 每个线程都拥有一个 Node.js 实例

就像下图:

深入了解Node.js的中Worker Threads(工作執行緒)

Worker_threads 模块允许使用多个线程来同时执行 JavaScript 代码。使用下面这个方式引入:

const worker = require('worker_threads');

Worker Threads 已经被添加到 Node.js 10 版本中,但是仍处于实验阶段。

使用 Worker threads 我们可以在在同一个进程内可以拥有多个 Node.js 实例,并且线程可以不需要跟随父进程的终止的时候才被终止,它可以在任意时刻被终止。当 Worker 线程销毁的时候分配给该 Worker 线程的资源依然没有被释放是一个很不好的操作,这会导致内存泄漏问题,我们也不希望这样。我们希望这些分配资源能够嵌入到 Node.js 中,让 Node.js 有创建线程的能力,并且在线程中创建一个新的 Node.js 实例,本质上就像是在同一个进程中运行多个独立的线程。

Worker Threads 有如下特性:

  • ArrayBuffers 可以将内存中的变量从一个线程转到另外一个
  • SharedArrayBuffer 可以在多个线程中共享内存中的变量,但是限制为二进制格式的数据。
  • 可用的原子操作,可以让你更有效率地同时执行某些操作并且实现竞态变量
  • 消息端口,用于多个线程间通信。可以用于多个线程间传输结构化的数据,内存空间
  • 消息通道就像多线程间的一个异步的双向通信通道。
  • WorkerData 是用于传输启动数据。在多个线程间使用 postMessgae 进行传输的时候,数据会被克隆,并将克隆的数据传输到线程的 contructor 中。

API:

  • const { worker, parantPort } = require('worker_threads'); =>worker 函数相当于一个独立的 JavaScript 运行环境线程,parentPort 是消息端口的一个实例
  • new Worker(filename) or new Worker(code, { eval: true }) =>启动 worker 的时候有两种方式,可以通过传输文件路径或者代码,在生产环境中推荐使用文件路径的方式。
  • worker.on('message'),worker.postMessage(data) => 这是多线程间监听事件与推送数据的方式。
  • parentPort.on('message'), parentPort.postMessage(data) => 在线程中使用 parentPort.postMessage 方式推送的数据可以在父进程中使用 worker.on('message') 的方式接收到,在父进程中使用 worker.postMessage() 的方式推送的数据可以在线程中使用 parentPort.on('message') 的方式监听到。

例子

const { Worker } = require('worker_threads');

const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.once('message',
    message => parentPort.postMessage({ pong: message }));  
`, { eval: true });
worker.on('message', message => console.log(message));      
worker.postMessage('ping');
$ node --experimental-worker test.js
{ pong: ‘ping’ }

上面例子所做的也就是使用 new Worker 创建一个线程,线程中的代码监听了 parentPort 的消息,并且当接收到数据的时候只触发一次回调,将收到的数据传输回父进程中。

你需要使用 --experimental-worker 启动程序因为 Workers 还在实验阶段。

另一个例子:

const {
	Worker, isMainThread, parentPort, workerData
} = require('worker_threads');

if (isMainThread) {
    module.exports = function parseJSAsync(script) {
        return new Promise((resolve, reject) => {
        	const worker = new Worker(filename, {
        		workerData: script
    		});
            worker.on('message', resolve);
            worker.on('error', reject);
            worker.on('exit', (code) => {
                if (code !== 0)
                    reject(new Error(`Worker stopped with exit code ${code}`));
            });
         });
    };
} else {
    const { parse } = require('some-js-parsing-library');
    const script = workerData;
    parentPort.postMessage(parse(script));
}

上面代码中:

  • Worker: 相当于一个独立的 JavaScirpt 运行线程。
  • isMainThread: 如果为 true 的话说明代码不是运行在 Worker 线程中
  • parentPort: 消息端口被使用来进行线程间通信
  • workerData:被传入 worker 的 contructor 的克隆数据。

在实际使用中,应该使用线程池的方式,不然不断地创建 worker 线程的代价将会超过它带来的好处。

對於 Worker 的使用建議:

  • 傳輸原生的句柄例如 sockets,http 請求
  • 死鎖偵測。死鎖是一種多個進程間被阻塞的情況,原因是每一個進程都持有一部分資源並等待另一個進程釋放它所持有的資源。在 Workers Threads 中死鎖偵測是非常有用的特性
  • 更好的隔離,所以如果一個執行緒中受影響,它不會影響到其他執行緒。

對於Worker 的一些不好的想法:

  • #不要認為Workers 會帶來不可思議的速度提升,有時使用線程池會是更好的選擇。
  • 不要使用 Workers 來並行執行 I/O 操作。
  • 不要認為建立 Worker 流程​​的開銷是很低的。

最後

Chrome devTools 支援 Node.js 中的 Workers 執行緒特性。 worker_threads 是一個實驗模組,如果你需要在 Node.js 中執行 CPU 密集型的操作,目前不建議在生產環境中使用 worker 線程,可以使用進程池的方式來代替。

英文原文網址:https://nodesource.com/blog/worker-threads-nodejs

作者:Liz Parody

更多編程相關知識,請造訪:程式設計影片! !

以上是深入了解Node.js的中Worker Threads(工作執行緒)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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