一文看懂 Node.js 中的多執行緒和多進程[每日前端夜話0x107]
- 2020 年 1 月 14 日
- 筆記
翻譯:瘋狂的技術宅
作者:Nastassia Ovchinnikova
來源:flatlogic.com

Node.js 是一個免費的跨平台 JavaScript 運行時環境,儘管它本質上是單執行緒的,但是可以在後台使用多個執行緒來執行非同步程式碼。
由於 Node.js 的非阻塞性質,不同的執行緒執行不同的回調,這些回調首先委託給事件循環。Node.js 運行時負責處理所有這一切。
為什麼要使用NodeJS?
JavaScript 最初是作為一種單執行緒程式語言構建的,僅在 Web 瀏覽器中運行。這意味著在一個過程中,只有一組指令能夠在給定的時間執行。
僅在當前程式碼塊的執行完成後,才移至下一個程式碼塊。但是,JavaScript 的單執行緒性質使實現變得容易。
最初,JavaScript 對於僅用於向網站添加少量交互。所以並沒有對多執行緒的需求。但是時代已經變了,用戶要求也越來越高,JavaScript 已成為「Web 上流行的程式語言」。
多執行緒現在變得很普遍。由於 JavaScript 是單執行緒語言,因此無法在其中實現多執行緒。幸運的是,在這種情況下,有一個很好的解決方法:Node.js。
Node.js 框架並不少,這要歸功於 JavaScript 運行時環境(尤其是 JavaScript)的普遍流行。在繼續本文之前,讓我們了解一些有關 Node.js 的重要觀點:
- 可以用 send 函數將消息從子進程傳遞到其他子進程和主進程
- 支援 fork 多個進程
- 主進程和子進程之間不共享狀態
為什麼要 fork 進程?
在兩種情況下,我們需要 fork 一個流程:
- 通過將任務委派給其他進程來提高速度
- 用於釋放記憶體和卸載單個進程
可以將數據發送到子進程,也可以將其送回。
Node.js 的方式
Node.js 使用兩種類型的執行緒:
- 通過事件循環處理主執行緒,
- 工作池中有許多輔助執行緒
事件循環負責獲取回調或函數,並將其註冊以供將來執行。它與正確的 JavaScript 程式碼在同一執行緒中運行。一旦 JavaScript 操作阻塞了執行緒,事件循環也會被阻塞。
工作池是一個執行模型,負責產生和處理不同的執行緒。它同步執行任務,然後將結果返回到事件循環,最後事件循環將結果提供給回調。
總而言之,工作池負責非同步 I/O 操作,即與系統磁碟和網路的交互。像 fs 和 crypto 這樣的模組是使用工作池的主要模組。
由於工作池是在 libuv 庫(https://en.wikipedia.org/wiki/Libuv)中實現的,Node.js 在 JS 和 C++ 之間進行內部通訊時會稍有延遲。不過這幾乎是不可察覺的。
一切都很好,直到我們遇到同步執行複雜操作的要求。任何需要大量時間執行的函數都會導致主執行緒阻塞。
如果程式具有多個佔用大量 CPU 的函數,將會導致伺服器吞吐量的顯著下降。在最壞的情況下,伺服器將會失去響應,並且無法將任務委派給工作池。
諸如 AI、大數據和機器學習之類的領域無法從 Node.js 中受益,因為這些操作阻塞了主執行緒,並使伺服器失去響應。但是這隨著 Node.js v10.5.0 的到來而改變,該版本增加了對多執行緒的支援。
並發和 CPU 綁定任務的挑戰
在 JavaScript 中建立並發可能很困難。允許多個執行緒訪問相同的記憶體會導致競爭狀態,這不僅使故障難以重現,而且解決起來也很困難。
Node.js 最初被實現為基於非同步 I/O 的伺服器端平台。通過簡單地消除執行緒需求,這使很多事情變得容易。是的,Node.js 程式是單執行緒的,但不是典型的方式。
我們可以在 Node.js 中並行運行,但是不需要創建執行緒。作業系統和虛擬機共同並行使用 I/O,然後在需要將數據發送回 JavaScript 程式碼時,JS 程式碼在單個執行緒中運行。
除 JS 程式碼外,所有內容均在 Node.js 中並行運行。與非同步塊不同,JS 的同步塊總是一次執行一次。與程式碼執行相比,等待 JS 中產生 I/O 事件所話費的時間要多得多。
Node.js 程式僅調用所需的函數或回調,而不會阻止其他程式碼的執行。最初 JavaScript 和 Node.js 都不打算處理 CPU 密集型或 CPU 綁定的任務。
當程式碼最少時,執行將會是敏捷的。但是計算量越大,執行速度就越慢。
如果你仍然嘗試在 JS 和 Node 中完成 CPU 密集型任務,那麼將會使瀏覽器中的 UI 凍結並對所有 I/O 事件進行排隊處理。儘管如此,我們已經走了很遠。現在有了 worker_threads 模組。
worker_threads 模組使多執行緒變得簡單
Node.js v10.5.0 於 2018 年 6 月發布,引入了 worker_threads 模組。它有助於在流行的 JavaScript 運行時環境中實現並發。該模組允許創建功能齊全的多執行緒 Node.js 應用。
從技術上講,工作執行緒是在單獨的執行緒中產生的一些程式碼。要開始使用輔助執行緒,需要先導入 worker_threads 模組。之後需要創建 Worker 類的實例以創建工作執行緒。
創建 Worker 類的實例時,有兩個參數:
- 第一個參數提供擴展名 .js 或 .mjs 的文件路徑,其中包含工作程式執行緒的程式碼,
- 第二個參數提供了一個包含 workerData 屬性的對象,該屬性包含工作執行緒開始執行時將訪問的數據
輔助執行緒能夠調度多個消息事件。因此,回調方法優先於返回 promise。
工作執行緒之間的通訊是基於事件的,即偵聽器設置為在工作執行緒發送事件後立即調用。最常見的 4 個事件是:
worker.on('error', (error) => {});
- 當工作執行緒中有未捕獲的異常時發出。接下來工作執行緒終止,並且該錯誤可以作為回調中的第一個參數使用。
worker.on('exit', (exitCode) => {})
- 當輔助執行緒退出時發出。如果在工作執行緒中調用了
process.exit()
,則會將 exitCode 提供給回調。如果worker.terminate()
終止工作執行緒,則程式碼為 1。
worker.on('message', (data) => {});
- 當工作執行緒將數據發送到父執行緒時發出。
worker.on('online', () => {});
- 當工作執行緒停止解析 JS 程式碼並開始執行時發出。儘管不常用,但 online 事件在特定情況下可能會提供更多資訊。
使用工作執行緒的方式
有兩種使用工作執行緒的方法:
- 方法 1 – 涉及產生工作執行緒,執行其程式碼並將結果發送到父執行緒。此方法需要每次為新任務從頭創建新的 worker 執行緒。
- 方法 2 – 涉及生成 worker 執行緒並為消息事件設置偵聽器。每次觸發該消息時,輔助執行緒都會執行程式碼,並將結果發送回父執行緒。輔助執行緒保持活動狀態,以備將來使用。
方法 2 也被稱為工作池。這是因為該方法涉及創建 worker 的工作池,先讓他們等待,並在需要時去調度消息事件來執行任務。
由於從頭創建工作執行緒需要創建虛擬機以及解析和執行程式碼,因此官方 Node.js 文檔 建議採用方法 2。此外,方法 2 更為實用,比方法 1 更有效。
worker_threads 模組中可用的重要屬性
- isMainThread – 當不在工作執行緒內操作時,此屬性為 true。如果需要,則可以在 worker 文件的開頭包含一個簡單的 if 語句。這樣可以確保它僅作為工作執行緒運行。
- parentPort – MessagePort 的實例,用於與父執行緒進行通訊。
- threadId – 分配給工作執行緒的唯一標識符。
- workerData – 包含在 worker 執行緒的構造函數中的數據。
Node.js 中的多進程
為了使 Node.js 利用多核系統的功能,可以用一些進程。流行的 javascript 運行時環境中有稱被為 cluster 的模組,該模組提供對多進程的支援。
使用 cluster 模組可以產生多個子進程,這些子進程可以共享一個公共埠。當子進程投入使用時,使用 NodeJS 的系統可以處理更大的工作量。
後端的 Node.js
互聯網已經成為全球數以百萬計公司的首選平台。因此,為使一家企業發揮最大潛力,並在此過程中脫穎而出,必須擁有強大的網路形象。
這一切都始於一個強大而直觀的網站。要打造一個完美無瑕的網站,重要的是選擇最佳的前端和後端技術。儘管本質上是單執行緒的,但 Node.js 是開發後端 Web 服務(https://squareboat.com/services/web-development)的首選。
儘管有大量的後端多執行緒選擇,但知名公司還是喜歡 Node.js。這是因為 Node.js 提供了在 JavaScript 中使用多執行緒的變通方法,而 JavaScript 已經是「Web上最流行的程式語言」。
總結
worker_threads 模組提供了一種在 Node.js 程式中實現多執行緒的簡便方法。通過將繁重的計算委派給工作執行緒,可以顯著提高伺服器的吞吐量。
藉助對多執行緒的支援,Node.js 將繼續吸引越來越多的來自 AI、大數據和機器學習等計算密集型領域的開發人員、工程師和其他專業人員。
原文:https://flatlogic.com/blog/multi-threading-and-multiple-process-in-node-js/