[譯]理解 Node.js 的中 Worker Threads

原文:nodesource.com/blog/worker…node

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

當一個 Node.js 的應用啓動的同時,它會啓動以下模塊:編程

  • 一個進程
  • 一個線程
  • 事件循環機制
  • JS 引擎實例
  • Node.js 實例

一個進程:process 對象是一個全局變量,可在 Node.js 程序中任意地方訪問,並提供當前進程的相關信息。promise

一個線程:單線程意味着在當前進程中同一時刻只有一個指令在執行。瀏覽器

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

一個 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 實例

就像下圖:

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 線程,可使用進程池的方式來代替。


關注【IVWEB社區】公衆號獲取每週最新文章,通往人生之巔!

相關文章
相關標籤/搜索