之前看到 concurrency 和 parallelism 比較的文章,把相關的概念和 Node.js 如何應用做個整理
Concurrency 與 Parallelism
首先要說明 concurrency 興 parallelism 的不同: concurrency 是指工作在重疊的時間內執行;parallelism 則是工作在完全相同的時間內執行
以下圖為例:
在計算機組織的概念裡,concurrency 相當於 pipeline。把一件任務分成很多階段交給不同的單位處理,同一時間每個單位只處理一個工作。
而 parallelism 則是 CPU 多核心的概念,每個核心可各自處理任務,同一時間有多個核心消化請求。
了解這二個概念後,我們再來看 Javascript 和 Node.js
Javascript with Single Thread
多數人學習 Javascript 都是從瀏覽器或 Node.js 環境開始執行程式,教材都會強調 JS 為單執行緒語言。實務上,不管 JS 用在前端(瀏覽器)或後端(Node.js) 主要處理非同步事件,如滑鼠點擊按鈕,或前端送請求到後端查詢資料。
這裡就帶到 JS 有名的 callback function,可以設定某個事件發生時,程式要如何回應。如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| button.on("click", (event) => { console.log(`event: ${event}`); }); ```
上面這段程式碼告訢瀏覽器當 button 被點擊時,要做回應並印出 log。
## Node.js Event Loop
進到 Node.js 環節。它是以 [google V8 引擎](https://v8.dev/) 為核心,讓 javascript 撰寫的程式能提供 API service。
API service 主要處理由前端(或其它 service) 打進來的 request,回傳資料和運算結果。
這裡面需要不斷地監聽外界傳進來的 request,收到後要查詢 Cache/database,最後再回傳。
而 Node.js 是如何應付成千上萬的 request 呢? 答案就在 event loop 設計
以下為 Event loop 在 Node.js server 的概念圖
![Event loop in Node.js](https://media.geeksforgeeks.org/wp-content/uploads/20200224050909/nodejs2.png)
Event loop 是一個不斷地處理外界 request 的無窮迴圈。這些 request 被加進 event queue 後依序處理。
圖示的 thread pool 集中 Node.js 對 I/O 和系統操作的 thread (執行緒)。
從這個角度來看,Node.js 還是有使用多執行緒,只不過 developer 在寫 Node.js 程式時,多半是以單執行緒的方式完成工作。
而 Event loop 從 queue 拿取工作後會經過下列步驟:
![Event loop process](https://media.geeksforgeeks.org/wp-content/uploads/20200224062607/phasesofloop-300x240.png)
各步驟的細節可以到 [這篇文章](https://notes.andywu.tw/2020/%E5%AE%8C%E6%95%B4%E5%9C%96%E8%A7%A3node-js%E7%9A%84event-loop%E4%BA%8B%E4%BB%B6%E8%BF%B4%E5%9C%88/) 查看。
Node.js server 收到 request 後便將工作丟到 event queue 裡,隨即處理下一個 request。同時,event loop 經過數個步驟處理非同步和運算的流程。整個分工模式就像是 concurrency vs. parallelism 那張圖所描繪的 concurrency。
這也是為何 Node.js 不適合作運算量大的服務,當 event loop 在處理工作時,需要等運算結果出來才能完成 callback。在正常情況下,這種操作會讓 service 的 response 拉的很長。
## Multi-Thread in Node.js
看完 Node.js 在 concurrency 的實作,假如想在 Node.js 寫 multi-thread 程式,該怎麼做呢?
Node.js 提供二種原生的方式:
1. [Cluster](https://nodejs.org/api/cluster.html) 可產生隔離的 multi-process application 2. [worker_threads](https://nodejs.org/api/worker_threads.html) 適合每個 process 的隔離不是必要且 worker 之間需要資料交換
### Cluster
以下為簡單的範例說明如何使用 cluster 建立 multi-thread 版本的 HTTP server
``` javascript import cluster from 'node:cluster'; import http from 'node:http';
if (cluster.isPrimary()) { cluster.fork(); cluster.fork(); cluster.fork(); cluster.fork(); } else { http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(3000); }
|
cluster.isPrimary()
用來判斷是否在主程序
cluster.fork()
建立 worker
- 在每個 worker 建立 HTTP server 監聽 port 3000 並回應 hello world
如果細心看會發現每個 worker 都監聽同一個 port,為何這樣寫是可以的呢?
在 Node.js cluster 裡,任何對 listen()
的呼叫會讓 Node.js 去監聽主程序而不是 worker。
主程序預設會採 round-robin 方式分配新的請求,當主程序收到 request 時,會透過 IPC 傳遞給其它 worker。
以上的範例說明如何使用 cluster 建立多程序的程式,但如果需要在不同 worker 間共享資料得使用另一種方式。
worker_threads
要加快運算量大(CPU-intense)的流程時,worker_threads 能解決這個問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const { Worker, isMainThread, parentPort } = require('node:worker_threads');
const MAX_THREAD_COUNT = 4;
if (isMainThread) { let workingThreadNum = MAX_THREAD_COUNT; let count = 0; for (let i = 0; i < MAX_THREAD_COUNT; i++) { const worker = new Worker(__filename); worker.on("message", msg => { if (msg === "done") { if (--workingThreadNum === 0) { process.stdout.write(`count: ${count}`); } } else { process.stdout.write(msg.toString() + " "); count += 1; } }); } } else { const { hashFunc } = require("some-library"); for (let i = 1; i < 1000000 / MAX_THREAD_COUNT; i++) { const randomNum = random64(); parentPort.postMessage(hashFunc(randomNum)); } parentPort.postMessage("done"); }
|
- 使用 new Worker 建立一個 worker thread。
__filename
表示目前的檔案
- 建立4個 worker threads,並在最後一個收到完成任務的 worker 印出總共處理的數目
- 假如不是 isMainThread,引入某個 hash function 並透過
postMessage
傳送結果
parentPort
為 worker 的 MessagePort,提供雙向溝通,藉此和 main thread 交換訊息。
在操作 multi-thread 時,很重要的一點便是共享記億體的保護。這已超出本文主題,留待之後再寫文章補充。
Reference