不系统学习NodeJs之进程线程
从工作期间断断续续接触Node,从去年的3月份、10月份、又到今年的3月份,终于又决定要再仔细看看Node的相关。
不系统学习的各种时期笔记&参考记录于此。
参考:
Node.js 中文网
一篇文章构建你的 NodeJS 知识体系
线下书:深入浅出node.js 著-朴灵
一、进程与线程
疑问:
- 都说Node是单线程,为什么启动Node后可以看到线程池内是多线程的,为什么单线程的Node却能用child_process | cluster创建多进程 ?
- 父子进程之间的关系、通信。
- cluster和child_process什么关系,创建出来的子进程有什么区别
除上以外,网上找的相关的其他问题(面试可能会问)
Node.js是单线程吗?
Node.js 做耗时的计算时候,如何避免阻塞?
Node.js如何实现多进程的开启和关闭?
Node.js可以创建线程吗?
你们开发过程中如何实现进程守护的?
除了使用第三方模块,你们自己是否封装过一个多进程架构?
进程与线程
先从定义开始理解。
进程 Process是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器。进程是资源分配的最小单位。
我们启动一个服务、运行一个实例,就是开一个服务进程,例如 Java 里的 JVM 本身就是一个进程,Node.js 里通过 node app.js 开启一个服务进程,多进程就是进程的复制(fork),fork 出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才可数据共享。
线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。
单线程就是一个进程只开一个线程
Javascript 就是属于单线程,程序顺序执行(这里暂且不提JS异步),可以想象一下队列,前面一个执行完之后,后面才可以执行,当你在使用单线程语言编码时切勿有过多耗时的同步操作,否则线程会造成阻塞,导致后续响应无法处理。你如果采用 Javascript 进行编码时候,请尽可能的利用Javascript异步操作的特性。
// 开启一个Node服务进程
const http = require('http');
const server = http.createServer();
server.listen(3000,()=>{
process.title='测试进程';
console.log('进程id',process.pid)
})
则创建Node进程如下:
Node中的进程与线程
Node.js 是 Javascript 在服务端的运行环境,构建在 chrome 的 V8 引擎之上,基于事件驱动、非阻塞I/O模型,充分利用操作系统提供的异步 I/O 进行多任务的执行,适合于 I/O 密集型的应用场景,因为异步,程序无需阻塞等待结果返回,而是基于回调通知的机制,原本同步模式等待的时间,则可以用来处理其它任务。因此Node可以应用于高并发场景。
科普:在 Web 服务器方面,著名的 Nginx 也是采用此模式(事件驱动),避免了多线程的线程创建、线程上下文切换的开销,Nginx 采用 C 语言进行编写,主要用来做高性能的 Web 服务器,不适合做业务。
在单核 CPU 系统之上我们采用 单进程 + 单线程 的模式来开发。在多核 CPU 系统之上,可以通过 child_process.fork 开启多个进程(Node.js 在 v0.8 版本之后新增了Cluster 来实现多进程架构) ,即 多进程 + 单线程 模式。注意:开启多进程不是为了解决高并发,主要是解决了单进程模式下 Node.js CPU 利用率不足的情况,充分利用多核 CPU 的性能。
在上面的例子中,不知大家是否有发现:开启了一个进程,却有9个线程,一直以来大家都说Node是单线程语言,这是否自相矛盾了呢?(这个问题困惑了我很久哈哈哈)
Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。
- 主线程:编译、执行代码。
- 编译/优化线程:在主线程执行的时候,可以优化代码。
- 分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。
- 垃圾回收的几个线程。
所以大家常说的 Node 是单线程的指的是 JavaScript 的执行是单线程的(开发者编写的代码运行在单线程环境中),但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的因为libuv中有线程池的概念存在的,libuv会通过类似线程池的实现来模拟不同操作系统的异步调用,这对开发者来说是不可见的。
(简单一句话解释:JavaScript在单线程中运行。但是其他node.js底层的、非Javascript的代码,是跑在多线程环境上的,内部完成I/O任务的是线程池,而线程池是多线程的。)
Node是如何做到异步I/O的呢? 主要靠: 事件循环,观察者,请求对象和I/O线程池。
《深入浅出node.js》朴灵
那又引出了我新的问题:事件循环是在JS线程上执行的,其他异步执行的任务又在哪里执行呢?
根据查询资料,得出如下结论:
异步的任务需要看它的类型,如果是IO任务或者其他libuv可执行的异步任务,会被丢到libuv管理的线程池中异步执行,之后再返回消息到事件循环中的队列去消费。但是非异步任务,如果一定要使用同步任务呢?(CPU密集型),则会产生阻塞,可以使用nextTick分片到下一个tick中,或者使用node提供的子进程服务,去利用其他的CPU内核执行任务。
参考:
Node.js 软肋之 CPU 密集型任务
nodejs单线程、异步事件的理解
worker子进程
*Node.js中的多进程和多线程
cluster 和 child_process 的使用场景
cluster的底层是child_process实现的,好像是在集群中有区别,但是我没看懂也没找到。cluster比起child_process实现了一些优化集成,cluster模块使用内置的负载均衡来更好地处理线程之间的压力(比child_process的负载均衡方式做了优化)
具体的上面的文章已经非常详细了,我只是做了一些简单记录:
一、创建
使用fork()创建子进程,一般子进程个数不超过CPU内核数。
cluster模块同样调用fork方法来创建子进程,该方法与child_process中的fork是同一个方法。
child_process
const childProcess = require('child_process')
const cpuNum = require('os').cpus().length
for (let i = 0; i < cpuNum; i++) {
childProcess.fork('./worker.js')
}
console.log('Master: Hello world.')
cluster
cluster模块采用的是经典的主从模型,Cluster会创建一个master,然后根据你指定的数量复制出多个子进程,可以使用 cluster.isMaster 属性判断当前进程是master还是worker(工作进程)。由master进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。
开启多进程时候端口疑问讲解:如果多个Node进程监听同一个端口时会出现
Error:listen EADDRIUNS
的错误,而cluster模块为什么可以让多个子进程监听同一个端口呢?
原因是master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket具柄发送给子进程。
const cluster = require('cluster')
if (cluster.isMaster) {
/* master进程 */
const cpuNum = require('os').cpus().length
for (let i = 0; i < cpuNum; ++i) {
cluster.fork()
}
// 创建进程完成后输出提示信息
cluster.on('online', (worker) => {
console.log('Create worker-' + worker.process.pid)
})
// 子进程退出后重启
cluster.on('exit', (worker, code, signal) => {
console.log('[Master] worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal)
cluster.fork()
})
} else {
/* worker进程 */
const net = require('net')
net.createServer().on('connection', (socket) => {
// 利用setTimeout模拟处理请求时的操作耗时
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
}).listen(8080)
}
二、通信
child_process
child_process通信是通过on(‘message’)和send()来实现的。父进程中创建worker,直接在worker上挂载监听,子进程中通过process对象接口监听来自父进程的消息或者向父进程发送消息。
/src/master.js
const childProcess = require('child_process')
const worker = childProcess.fork('./worker.js')
worker.send('Hello world.')
worker.on('message', (msg) => {
console.log('[Master] Received message from worker: ' + msg)
})
/src/worker.js
process.on('message', (msg) => {
console.log('[Worker] Received message from master: ' + msg)
process.send('Hi master.')
})
输出
$ node index.js
[Worker] Received message from master: Hello world.
[Master] Received message from worker: Hi master.
cluster
同child_process
写了一个简单的NodeJS实现的进程间通信的例子
cluster.setttings:配置集群参数对象
cluster.isMaster:判断是不是master节点
cluster.isWorker:判断是不是worker节点
Event: 'fork': 监听创建worker进程事件
Event: 'online': 监听worker创建成功事件
Event: 'listening': 监听worker向master状态事件
Event: 'disconnect': 监听worker断线事件
Event: 'exit': 监听worker退出事件
Event: 'setup': 监听setupMaster事件
cluster.setupMaster([settings]): 设置集群参数
cluster.fork([env]): 创建worker进程
cluster.disconnect([callback]): 关闭worket进程
cluster.worker: 获得当前的worker对象
cluster.workers: 获得集群中所有存活的worker对象
worker对象
worker的各种属性和函数:可以通过cluster.workers, cluster.worket获得。
worker.id: 进程ID号
worker.process: ChildProcess对象
worker.suicide: 在disconnect()后,判断worker是否自杀
worker.send(message, [sendHandle]): master给worker发送消息。注:worker给发master发送消息要用process.send(message)
worker.kill([signal='SIGTERM']): 杀死指定的worker,别名destory()
worker.disconnect(): 断开worker连接,让worker自杀
Event: 'message': 监听master和worker的message事件
Event: 'online': 监听指定的worker创建成功事件
Event: 'listening': 监听master向worker状态事件
Event: 'disconnect': 监听worker断线事件
Event: 'exit': 监听worker退出事件
负载均衡
上方链接有介绍,没整特别明白,不多记载了。
进程守护
每次启动 Node.js 程序都需要在命令窗口输入命令 node app.js 才能启动,但如果把命令窗口关闭则Node.js 程序服务就会立刻断掉。除此之外,当我们这个 Node.js 服务意外崩溃了就不能自动重启进程了。这些现象都不是我们想要看到的,所以需要通过某些方式来守护这个开启的进程,执行 node app.js 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。当出现问题可以自动重启。
worker进程可能因为某些异常情况而退出,为了提高集群的稳定性,master进程需要监听子进程的存活状态,当子进程退出之后,master进程要及时重启新的子进程。在Node中,子进程退出时,会在父进程中触发exit事件。父进程只需通过监听该事件便可知道子进程是否退出,并在退出的时候做出相应的处理。
// 例子
// 创建工作进程
let workers = []
let cur = 0
for (let i = 0; i < cpuNum; ++i) {
const newProcess = childProcess.fork('./worker.js')
workers.push(newProcess)
console.log('Create worker-' + workers[i].pid)
// 工作进程退出后重启
newProcess.on('exit', () => {
console.log('Worker-' + newProcess.pid + ' exited')
workers[i] = childProcess.fork('./worker.js')
console.log('Create worker-' + childProcess.pid)
})
}
或者使用第三方依赖实现进程守护。pm2 和 forever ,底层也都是通过上面讲的 child_process 模块和 cluster 模块 实现的。