[TOC]
# 前置知識
> JavaScript 代碼在計算機進程中的單個線程上執行。它的代碼在這個線程上進行同步處理,每次只運行一條指令。因此,如果我們要在這個線程上執行一個長時間運行的任務,那么后續的代碼都將被阻塞,直到任務完成。
Node.js 遵循的是單線程單進程的模式,node 的單線程是指 js 的引擎只有一個實例,且在 nodejs 的主線程中執行,同時 node 以事件驅動的方式處理 IO 等異步操作。
node 的單線程模式,只維持一個主線程,大大減少了線程間切換的開銷。
它的優勢是沒有線程間數據同步的性能消耗也不會出現死鎖的情況。所以它是線程安全并且性能高效的。
單線程有它的弱點,以單一進程運行,無法充分利用多核 CPU 資源,**CPU 密集型計算**(即只用 CPU 計算的操作,比如要對數據加解密(node.bcrypt.js),數據壓縮和解壓 (node-tar))可能會導致 I/O 阻塞,以及出現錯誤可能會導致應用崩潰。
# 解決單線程弱點
## 瀏覽器端
HTML5 制定了 Web Worker 標準(Web Worker 的作用,就是為 JavaScript 創造多線程環境,允許主線程創建 Worker 線程,將一些任務分配給后者運行)。
> http://www.ruanyifeng.com/blog/2018/07/web-worker.html
## Node 端
采用了和 Web Worker 相同的思路來解決單線程中大量計算問題 ,官方提供了 `child_process` 模塊和 `cluster` 模塊。
1. `cluster` 模塊:為了調度多核 CPU 等資源,利用多核 CPU 的資源,使得可以通過一串 node 子進程去處理負載任務,同時保證一定的負載均衡型。
2. `child_process` 模塊:為了進行 CPU 密集型操作,不阻塞主線程。**創建獨立的子進程**,父子進程通過 **IPC 通信**,子進程可以是外部應用也可以是 node 子程序,子進程執行后可以將結果返回給父進程。
`child_process`、`cluster` (底層是基于 `child_process` 實現),都是用于創建子進程,然后子進程間通過事件消息來傳遞結果,這個可以很好地保持應用模型的簡單和低依賴。
## `child_process` 模塊
`child_process`的實例,表示一個系統子進程,并執行 shell 命令,在與系統層面的交互上挺有用處。
NodeJS 子進程提供了與系統交互的重要接口,其主要 API 有:
* 標準輸入、標準輸出及標準錯誤輸出的接口
* `child.stdin`?獲取標準輸入
* `child.stdout`?獲取標準輸出
* `child.stderr`?獲取標準錯誤輸出
* 獲取子進程的PID:`child.pid`
* 提供生成子進程的重要方法:`child_process.spawn(cmd, args=[], [options])`
* 提供直接執行系統命令的重要方法:`child_process.exec(cmd, [options], callback)`
* 提供殺死進程的方法:`child.kill(signal='SIGTERM')`
下面都是默認異步創建子進程的方式,,子進程的運行不會阻塞主進程,每一種方式都有對應的同步版本(`execFileSync`、`spawnSync` 和 `execSync`)。
* `.exec()`、`.execFile()`、`.fork()`底層都是通過`.spawn()`實現的。
* `.exec()`、`execFile()`額外提供了回調,當子進程停止的時候執行。
```
child_process.spawn(command[, args][, options])
child_process.exec(command[, options][, callback])
child_process.execFile(file[, args][, options][, callback])
child_process.fork(modulePath[, args][, options])
```
`child_process.spawn`利用命令行創建一個子進程,并且可以控制子進程的啟動,終止,以及通信:
```javascript
/***************
* spawn 創建了一個子進程,并返回一個進程描述符,即句柄
* 進程句柄都有一個 stdout 屬性,以流的形式輸出進程的標準輸出信息
* 可以在這個輸出流上綁定事件,監視每個輸出
* ****************/
// tail 命令會監控一個文件(不存在則退出),
// 如果文件發生改變則在標準輸出流中輸出文件內容
let spawn = require('child_process').spawn;
// 創建一個子進程,將進程描述符賦值給child
let child = spawn('tail', ['-f', './test']);
// 監聽標準輸出流
child.stdout.on('data', function (data) {
console.log('tail output: ' + data);
});
// 終止進程
setTimeout(() => {
// 默認發送 SIGTERM
child.kill();
}, 1000);
// 監聽子進程退出事件
child.on('exit', (code, signal) => {
if (code) {
// 正常退出會有一個退出碼,0為正常退出,非0一般表示錯誤
console.log('child process terminated with code ' + code);
} else {
// 非正常退出,輸出退出信號
console.log('child process terminated with signal ' + signal);
}
});
```
??示例中 `child.on('exit', (code, signal) => {})
`:
參數:`code`、`signal`,如果子進程是自己退出的,那么`code`就是退出碼,否則為`null`;如果子進程是通過信號結束的,那么,`signal`就是結束進程的信號,否則為`null`。這兩者中,肯定有一個不為`null`。
注意事項:`exit`事件觸發時,子進程的 stdio stream 可能還打開著。(場景?)此外,node 監聽了`SIGINT`和`SIGTERM`信號,也就是說,node 收到這兩個信號時,不會立刻退出,而是先做一些清理的工作,然后重新拋出這兩個信號。(目測此時js可以做清理工作了,比如關閉數據庫等。)
* SIGINT:`interrupt`,程序終止信號,通常在用戶按下`CTRL+C`時發出,用來通知前臺進程終止進程。
* SIGTERM:`terminate`,程序結束信號,該信號可以被阻塞和處理,通常用來要求程序自己正常退出。shell 命令`kill`默認產生這個信號。如果信號終止不了,我們才會嘗試`SIGKILL`(強制終止)。
> [child_process](https://www.jianshu.com/p/03bbc306088e)
## `cluster` 模塊
根據多核 CPU 創建子進程后,自動控制負載均衡的方式。
我們將 master 稱為主進程,而 worker 進程稱為工作進程,利用 `cluster` 模塊,使用 node 封裝好的 API、**IPC 通道**和調度機可以非常簡單的創建包括一個 master 進程下 HTTP 代理服務器 + 多個 worker 進程多個 HTTP 應用服務器的架構。
從官網的例子來看:
```
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主進程 ${process.pid} 正在運行`);
// 衍生工作進程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作進程 ${worker.process.pid} 已退出`);
});
} else {
// 工作進程可以共享任何 TCP 連接。
// 在本例子中,共享的是一個 HTTP 服務器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作進程 ${process.pid} 已啟動`);
}
```
最后輸出的結果為:
```
$ node server.js
主進程 3596 正在運行
工作進程 4324 已啟動
工作進程 4520 已啟動
工作進程 6056 已啟動
工作進程 5644 已啟動
```
### 重要代表 pm2
pm2優點很多:
* 負載均衡
* 熱重載:0s reload
* 非常好的測試覆蓋率
pm2 啟動很簡單:
```
$ pm2 start server.js -i 4 -l ./log.txt
```
* `-i 4` 是 cpu數量(我是4核的)
* `-l ./log.txt` 打日志
pm2 啟動后自動到后臺執行 通過?`$ pm2 list`?可以查看正在跑著那些進程。
更多內容直接看官網:?http://pm2.keymetrics.io/
# 多線程
Node V10.5.0: 提供了實驗性質的 `worker_threads` 模塊,才讓 Node 擁有了多工作線程。
Node V12.0.0:`worker_threads` 已經成為正式標準,可以在生產環境放心使用。
也有很多開發者認為 `worker_threads` 違背了 nodejs 設計的初衷,事實上那是它并沒有真正理解 `worker_threads` 的底層原理。
## `worker_threads` 模塊
`worker_threads` (工作線程)對于執行 CPU 密集型的 JavaScript 操作非常有用。它們對 I/O 密集型工作沒有多大幫助。js 的內置異步 I/O 操作比 Workers 效率更高。
`worker_threads` 比使用 `child_process` 或 cluster 可以獲得的并行性更輕量級。 此外,`worker_threads` 可以有效地共享內存。通過傳輸 ArrayBuffer 實例或共享 SharedArrayBuffer 實例來實現。
1. 加載 `worker_threads` 模塊
node.js v10.5.0 引入的實驗性質 API,開啟時需要使用 `--experimental-worker` 參數。
node.js v12.0.0 里面默認開啟,也預示著您可以將該特性用于生產環境中。
2. 示例
```
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
// This code is executed in the main thread and not in the worker.
// Create the worker.
const worker = new Worker(__filename);
// Listen for messages from the worker and print them.
worker.on('message', (msg) => { console.log(msg); });
} else {
// This code is executed in the worker and not in the main thread.
// Send a message to the main thread.
parentPort.postMessage('Hello world!');
}
```
* Worker: 該類用于創建 worker 對象。有一個必填參數`__filename`(文件路徑),該文件會被 worker 執行。同時我們可以在主線程中通過 `worker.on` 監聽 `message` 事件
* isMainThread: 該對象用于區分是主線程(true)還是工作線程(false)
* parentPort: 該對象的 `postMessage` 方法用于 worker 線程向主線程發送消息
# 進程通信方式
## 通過`stdin/stdout`等傳遞
## 原生 IPC 方式
## 通過網絡 Sockets
# 參考
[Nodejs 學習筆記以及經驗總結/cluster.md](https://github.com/chyingp/nodejs-learning-guide/blob/master/%E6%A8%A1%E5%9D%97/cluster.md)
[nodejs-learning-guide](https://github.com/chyingp/nodejs-learning-guide/blob/master/%E6%A8%A1%E5%9D%97/child_process.md)