不系统学习NodeJs之进程线程

从工作期间断断续续接触Node,从去年的3月份、10月份、又到今年的3月份,终于又决定要再仔细看看Node的相关。
不系统学习的各种时期笔记&参考记录于此。

参考:
Node.js 中文网
一篇文章构建你的 NodeJS 知识体系
线下书:深入浅出node.js 著-朴灵

一、进程与线程

参考:
深入理解Node.js 进程与线程

疑问:

  1. 都说Node是单线程,为什么启动Node后可以看到线程池内是多线程的,为什么单线程的Node却能用child_process | cluster创建多进程 ?
  2. 父子进程之间的关系、通信。
  3. 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 模块 实现的。

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值