[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)