淺析 Node 進程與線程

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程與線程是操作系統中兩個重要的角色,它們維繫着不同程序的執行流程,通過系統內核的調度,完成多任務執行。今天我們從 Node.js(以下簡稱 Node)的角度來一起學習相關知識,通過本文讀者將瞭解 Node 進程與線程的特點、代碼層面的使用以及它們之間的通信。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概念"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我們還是回顧一下相關的定義:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"進程"},{"type":"text","text":"是一個具有一定獨立功能的程序在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程序運行的載體。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"線程"},{"type":"text","text":"是程序執行中一個單一的順序控制流,它存在於進程之中,是比進程更小的能獨立運行的基本單位。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"早期在單核 CPU 的系統中,爲了實現多任務的運行,引入了進程的概念,不同的程序運行在數據與指令相互隔離的進程中,通過時間片輪轉調度執行,由於 CPU 時間片切換與執行很快,所以看上去像是在同一時間運行了多個程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於進程切換時需要保存相關硬件現場、進程控制塊等信息,所以系統開銷較大。爲了進一步提高系統吞吐率,在同一進程執行時更充分的利用 CPU 資源,引入了線程的概念。線程是操作系統調度執行的最小單位,它們依附於進程中,共享同一進程中的資源,基本不擁有或者只擁有少量系統資源,切換開銷極小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"單線程?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們常常聽到有開發者說 “Node.js 是單線程的”,那麼 Node 確實是只有一個線程在運行嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,執行以下 Node 代碼(示例一):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 示例一\nrequire('http').createServer((req, res) => {\n res.writeHead(200);\n res.end('Hello World');\n}).listen(8000);\nconsole.log('process id', process.pid);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Node 內建模塊 http 創建了一個監聽 8000 端口的服務,並打印出該服務運行進程的 pid,控制檯輸出 pid 爲 35919(可變),然後我們通過命令 "},{"type":"codeinline","content":[{"type":"text","text":"top -pid 35919"}]},{"type":"text","text":" 查看進程的詳細信息,如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"PID COMMAND %CPU TIME #TH #WQ #POR MEM PURG CMPRS PGRP PPID STATE BOOSTS %CPU_ME\n35919 node 0.0 00:00.09 7 0 35 8564K 0B 8548K 35919 35622 sleeping *0[1] 0.00000\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們看到 "},{"type":"codeinline","content":[{"type":"text","text":"#TH"}]},{"type":"text","text":" (threads 線程) 這一列顯示此進程中包含 7 個線程,"},{"type":"text","marks":[{"type":"strong"}],"text":"說明 Node 進程中並非只有一個線程"},{"type":"text","text":"。事實上一個 Node 進程通常包含:1 個 Javascript 執行主線程;1 個 watchdog 監控線程用於處理調試信息;1 個 v8 task scheduler 線程用於調度任務優先級,加速延遲敏感任務執行;4 個 v8 線程(可參考以下代碼),主要用來執行代碼調優與 GC 等後臺任務;以及用於異步 I\/O 的 libuv 線程池。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ v8 初始化線程\nconst int thread_pool_size = 4; \/\/ 默認 4 個線程\ndefault_platform = v8::platform::CreateDefaultPlatform(thread_pool_size);\nV8::InitializePlatform(default_platform);\nV8::Initialize();\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中異步 I\/O 線程池,如果執行程序中不包含 I\/O 操作如文件讀寫等,則默認線程池大小爲 0,否則 Node 會初始化大小爲 4 的異步 I\/O 線程池,當然我們也可以通過 "},{"type":"codeinline","content":[{"type":"text","text":"process.env.UV_THREADPOOL_SIZE"}]},{"type":"text","text":" 自己設定線程池大小。需要注意的是在 Node 中網絡 I\/O 並不佔用線程池。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖爲 Node 的進程結構圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/92\/928697fd72094f54df234be3c428026f.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了驗證上述分析,我們運行示例二的代碼,加入文件 I\/O 操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 示例二\nrequire('fs').readFile('.\/test.log', err => {\n if (err) {\n console.log(err);\n process.exit();\n } else {\n console.log(Date.now(), 'Read File I\/O');\n }\n});\nconsole.log(process.pid);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後得到如下結果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"PID COMMAND %CPU TIME #TH #WQ #POR MEM PURG CMPR PGRP PPID STATE BOOSTS %CPU_ME %CPU_OTHRS\n39443 node 0.0 00:00.10 11 0 39 8088K 0B 0B 39443 35622 sleeping *0[1] 0.00000 0.00000\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時 "},{"type":"codeinline","content":[{"type":"text","text":"#TH"}]},{"type":"text","text":" 一欄的線程數變成了 11,即大小爲 4 的 I\/O 線程池被創建。至此,我們針對段首的問題心裏有了答案,"},{"type":"text","marks":[{"type":"strong"}],"text":"Node 嚴格意義講並非只有一個線程,通常說的 “Node 是單線程” 其實是指 JS 的執行主線程只有一個"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"事件循環"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然 JS 執行線程只有一個,那麼 Node 爲什麼還能支持較高的併發?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上文異步 I\/O 我們也能獲得一些思路,Node 進程中通過 libuv 實現了一個事件循環機制(uv_event_loop),當執行主線程發生阻塞事件,如 I\/O 操作時,主線程會將耗時的操作放入事件隊列中,然後繼續執行後續程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"uv_event_loop 嘗試從 libuv 的線程池(uv_thread_pool)中取出一個空閒線程去執行隊列中的操作,執行完畢獲得結果後,通知主線程,主線程執行相關回調,並且將線程實例歸還給線程池。通過此模式循環往復,來保證非阻塞 I\/O,以及主線程的高效執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相關流程可參照下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/85\/854d7464b311bdb30de77dada7ffd9ca.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"子進程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過事件循環機制,Node 實現了在 I\/O 密集型(I\/O-Sensitive)場景下的高併發,但是如果代碼中遇到 CPU 密集場景(CPU-Sensitive)的場景,那麼主線程將長時間阻塞,無法處理額外的請求。爲了應對 CPU-Sensitive 場景,以及充分發揮 CPU 多核性能,Node 提供了 child_process 模塊(官方文檔,"},{"type":"text","marks":[{"type":"underline"}],"text":"https:\/\/nodejs.org\/api\/child_process.html"},{"type":"text","text":")進行進程的創建、通信、銷燬等等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"創建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"child_process 模塊提供了 4 種異步創建 Node 進程的方法,具體可參考 child_process API,這裏做一下簡要介紹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"spawn 以主命令加參數數組的形式創建一個子進程,子進程以流的形式返回 data 和 error 信息。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"exec 是對 spawn 的封裝,可直接傳入命令行執行,以 callback 形式返回 error stdout stderr 信息"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"execFile 類似於 exec 函數,但默認不會創建命令行環境,將直接以傳入的文件創建新的進程,性能略微優於 exec"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"fork 是 spawn 的特殊場景,只能用於創建 node 程序的子進程,默認會建立父子進程的 IPC 信道來傳遞消息"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Linux 系統中,可以通過管道、消息隊列、信號量、共享內存、Socket 等手段來實現進程通信。在 Node 中,父子進程可通過 IPC (Inter-Process Communication) 信道收發消息,IPC 由 libuv 通過管道 pipe 實現。一旦子進程被創建,並設置父子進程的通信方式爲 IPC(參考 stdio 設置),父子進程即可雙向通信。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程之間通過 "},{"type":"codeinline","content":[{"type":"text","text":"process.send"}]},{"type":"text","text":" 發送消息,通過監聽 "},{"type":"codeinline","content":[{"type":"text","text":"message"}]},{"type":"text","text":" 事件接收消息。當一個進程發送消息時,會先序列化爲字符串,送入 IPC 信道的一端,另一個進程在另一端接收消息內容,並且反序列化,因此我們可以在進程之間傳遞對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"示例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是 Node.js 創建進程和通信的一個基礎示例,主進程創建一個子進程並將計算斐波那契數列的第 44 項這一 CPU 密集型的任務交給子進程,子進程執行完成後通過 IPC 信道將結果發送給主進程:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"main_process.js"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 主進程\nconst { fork } = require('child_process');\nconst child = fork('.\/fib.js'); \/\/ 創建子進程\nchild.send({ num: 44 }); \/\/ 將任務執行數據通過信道發送給子進程\nchild.on('message', message => {\n console.log('receive from child process, calculate result: ', message.data);\n child.kill();\n});\nchild.on('exit', () => {\n console.log('child process exit');\n});\nsetInterval(() => { \/\/ 主進程繼續執行\n console.log('continue excute javascript code', new Date().getSeconds());\n}, 1000);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"fib.js"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 子進程 fib.js\n\/\/ 接收主進程消息,計算斐波那契數列第 N 項,併發送結果給主進程\n\/\/ 計算斐波那契數列第 n 項\nfunction fib(num) {\n if (num === 0) return 0;\n if (num === 1) return 1;\n return fib(num - 2) + fib(num - 1);\n}\nprocess.on('message', msg => { \/\/ 獲取主進程傳遞的計算數據\n console.log('child pid', process.pid);\n const { num } = msg;\n const data = fib(num);\n process.send({ data }); \/\/ 將計算結果發送主進程\n});\n\/\/ 收到 kill 信息,進程退出\nprocess.on('SIGHUP', function() {\n process.exit();\n});\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"child pid 39974\ncontinue excute javascript code 41\ncontinue excute javascript code 42\ncontinue excute javascript code 43\ncontinue excute javascript code 44\nreceive from child process, calculate result: 1134903170\nchild process exit\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"集羣模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了更加方便的管理進程、負載均衡以及實現端口複用,Node 在 v0.6 之後引入了 cluster 模塊(官方文檔,"},{"type":"text","marks":[{"type":"underline"}],"text":"https:\/\/nodejs.org\/api\/cluster.html"},{"type":"text","text":"),相對於子進程模塊,cluster 實現了單 master 主控節點和多 worker 執行節點的通用集羣模式。cluster master 節點可以創建銷燬進程並與子進程通信,子進程之間不能直接通信;worker 節點則負責執行耗時的任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cluster 模塊同時實現了負載均衡調度算法,在類 unix 系統中,cluster 使用輪轉調度(round-robin),node 中維護一個可用 worker 節點的隊列 free,和一個任務隊列 handles。當一個新的任務到來時,節點隊列隊首節點出隊,處理該任務,並返回確認處理標識,依次調度執行。而在 win 系統中,Node 通過 Shared Handle 來處理負載,通過將文件描述符、端口等信息傳遞給子進程,子進程通過信息創建相應的 SocketHandle \/ ServerHandle,然後進行相應的端口綁定和監聽,處理請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cluster 大大的簡化了多進程模型的使用,以下是使用示例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 計算斐波那契數列第 43 \/ 44 項\nconst cluster = require('cluster');\n\/\/ 計算斐波那契數列第 n 項\nfunction fib(num) {\n if (num === 0) return 0;\n if (num === 1) return 1;\n return fib(num - 2) + fib(num - 1);\n}\nif (cluster.isMaster) { \/\/ 主控節點邏輯\n for (let i = 43; i < 45; i++) {\n const worker = cluster.fork() \/\/ 啓動子進程\n \/\/ 發送任務數據給執行進程,並監聽子進程回傳的消息\n worker.send({ num: i });\n worker.on('message', message => {\n console.log(`receive fib(${message.num}) calculate result ${message.data}`)\n worker.kill();\n });\n }\n \n \/\/ 監聽子進程退出的消息,直到子進程全部退出\n cluster.on('exit', worker => {\n console.log('worker ' + worker.process.pid + ' killed!');\n if (Object.keys(cluster.workers).length === 0) {\n console.log('calculate main process end');\n }\n });\n} else {\n \/\/ 子進程執行邏輯\n process.on('message', message => { \/\/ 監聽主進程發送的信息\n const { num } = message;\n console.log('child pid', process.pid, 'receive num', num);\n const data = fib(num);\n process.send({ data, num }); \/\/ 將計算結果發送給主進程\n })\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"工作線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Node v10 以後,爲了減小 CPU 密集型任務計算的系統開銷,引入了新的特性:工作線程 worker_threads(官方文檔,"},{"type":"text","marks":[{"type":"underline"}],"text":"https:\/\/nodejs.org\/api\/worker_threads.html"},{"type":"text","text":")。通過 worker_threads 可以在進程內創建多個線程,主線程與 worker 線程使用 parentPort 通信,worker 線程之間可通過 MessageChannel 直接通信。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"創建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 worker_threads 模塊中的 Worker 類我們可以通過傳入執行文件的路徑創建線程。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const { Worker } = require('worker_threads');\n...\nconst worker = new Worker(filepath);\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"通信"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"使用 parentPort 進行父子線程通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"worker_threads 中使用了 MessagePort(繼承於 EventEmitter,參考:"},{"type":"text","marks":[{"type":"underline"}],"text":"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/MessagePort"},{"type":"text","text":")來實現線程通信。worker 線程實例上有 parentPort 屬性,是 MessagePort 類型的一個實例,子線程可利用 postMessage 通過 parentPort 向父線程傳遞數據,示例如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const { Worker, isMainThread, parentPort } = require('worker_threads');\n\/\/ 計算斐波那契數列第 n 項\nfunction fib(num) {\n if (num === 0) return 0;\n if (num === 1) return 1;\n return fib(num - 2) + fib(num - 1);\n}\nif (isMainThread) { \/\/ 主線程執行函數\n const worker = new Worker(__filename);\n worker.once('message', (message) => {\n const { num, result } = message;\n console.log(`Fibonacci(${num}) is ${result}`);\n process.exit();\n });\n worker.postMessage(43);\n console.log('start calculate Fibonacci');\n \/\/ 繼續執行後續的計算程序\n setInterval(() => {\n console.log(`continue execute code ${new Date().getSeconds()}`);\n }, 1000);\n} else { \/\/ 子線程執行函數\n parentPort.once('message', (message) => {\n const num = message;\n const result = fib(num);\n \/\/ 子線程執行完畢,發消息給父線程\n parentPort.postMessage({\n num,\n result\n });\n });\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"start calculate Fibonacci\ncontinue execute code 8\ncontinue execute code 9\ncontinue execute code 10\ncontinue execute code 11\nFibonacci(43) is 433494437\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用 MessageChannel 實現線程間通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"worker_threads 還可以支持線程間的直接通信,通過兩個連接在一起的 MessagePort 端口,worker_threads 實現了雙向通信的 MessageChannel。線程間可通過 postMessage 相互通信,示例如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"const {\n isMainThread, parentPort, threadId, MessageChannel, Worker\n} = require('worker_threads');\n \nif (isMainThread) {\n const worker1 = new Worker(__filename);\n const worker2 = new Worker(__filename);\n \/\/ 創建通信信道,包含 port1 \/ port2 兩個端口\n const subChannel = new MessageChannel();\n \/\/ 兩個子線程綁定各自信道的通信入口\n worker1.postMessage({ port: subChannel.port1 }, [ subChannel.port1 ]);\n worker2.postMessage({ port: subChannel.port2 }, [ subChannel.port2 ]);\n} else {\n parentPort.once('message', value => {\n value.port.postMessage(`Hi, I am thread${threadId}`);\n value.port.on('message', msg => {\n console.log(`thread${threadId} receive: ${msg}`);\n });\n });\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"thread2 receive: Hi, I am thread1\nthread1 receive: Hi, I am thread2\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"注意"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"worker_threads 只適用於進程內部 CPU 計算密集型的場景,而不適合於 I\/O 密集場景,針對後者,官方建議使用進程的 event_loop 機制,將會更加高效可靠。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Node.js 本身設計爲單線程執行語言,通過 libuv 的線程池實現了高效的非阻塞異步 I\/O,保證語言簡單的特性,儘量減少編程複雜度。但是也帶來了在多核應用以及 CPU 密集場景下的劣勢,爲了補齊這塊短板,Node 可通過內建模塊 child_process 創建額外的子進程來發揮多核的能力,以及在不阻塞主進程的前提下處理 CPU 密集任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了簡化開發者使用多進程模型以及端口複用,Node 又提供了 cluster 模塊實現主-從節點模式的進程管理以及負載調度。由於進程創建、銷燬、切換時系統開銷較大,worker_threads 模塊又隨之推出,在保持輕量的前提下,可以利用更少的系統資源高效地處理 進程內 CPU 密集型任務,如數學計算、加解密,進一步提高進程的吞吐率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:BrunoLee"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/P9k8SIIVrw6rV2Bvit_IMQ"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:淺析 Node 進程與線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:政採雲前端團隊 - 微信公衆號 [ID:Zoo-Team]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章