企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[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)