浅谈nodejs进程和线程

569 阅读5分钟

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

正文

进程概念

进程是操作系统进行资源分配和调度的基本单位,是对操作系统上运行程序的一个抽象。每个进程拥有各自独立的内存空间,使得各个进程之间内存地址相互隔离。

这点很好理解,当运行一个程序时,首先会从磁盘中读取代码到内存中,由CPU去执行代码,运行过程中会涉及数据的存放、以及与外部设备交互等,这便涉及到程序对计算机资源的使用。那系统上存在那么多程序,且CPU只有一个,为了便于管理程序资源的使用,操作系统会把每个运行中的程序封装为一个实体,分配各自所需的资源,再根据调度算法切换执行。这里的实体,便是“进程”。

线程概念

当程序的功能越来越复杂,程序内部存在多种功能模块,比如网络请求、IO操作等,我们希望能将应用程序分解成更细粒度、能准并行运行多个顺序执行实体,并且这些细粒度的实体能够共享进程的内存空间(程序代码、数据、内存空间等),因此就诞生了“线程”。

每个进程都有独立的代码和数据空间(程序上下文),程序之间切换会有较大开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有独立的运行栈和程序计算器,线程间的切换开销小。所以线程的创建、销毁、调度性能远优于进程。

线程是操作系统运算调度的最小单位,线程隶属于进程。一个线程只能隶属于一个进程,但一个进程能够拥有多个线程。

简而言之,进程负责分配和管理系统资源,线程负责CPU调度运算,也是CPU切换时间片的最小单位。

node.js处理耗时操作

通过fork子进程处理耗时操作,子进程执行完耗时操作,再通过send方法将结果发送给主进程,主进程通过message监听信息后处理并退出。

通过child_process模块创建子进程处理耗时操作的例子:

// forkApp.js
const http = require('http')
const fork = require('child_process').fork

const server = http.createServer((req,res)=>{
  if(req.url === '/sum'){
    // 开启一个子进程
    const sum = fork('./fork_sum.js')
    // 向子进程发送消息
    sum.send('start')
    // 监听子进程消息
    sum.on('message',sum=>{
      res.end({sum})
      sum.kill() // 执行完成,杀死子进程
    })
    // 监听子进程错误消息
    sum.on('close',(code,signal)=>{
      console.log('监听到close事件',code,signal)
      sum.kill()
    })
  }else{res.end('ok')}
})
// fork_sum.js
const sum = ()=>{
  let total = 0
  for (let i = 0; i < 1e10; i++) {	
     total += i	
  }
  return total
}

process.on('message',msg=>{
  const total = sum()
  process.send(total)
})

node.js多进程架构

Node.js通过node app.js开启一个服务进程,多进程是通过进程的复制(fork),fork出来的每个子进程都拥有各自独立的空间地址、数据栈,并且进程之间是相互独立,无法访问彼此的变量、数据结构。进程之间只有建立IPC通道才能共享数据。

这里采用多进程架构并非是用于解决高并发问题,而是处理单进程模式下CPU利用率不足的情况。核心是,父进程负责监听端口,接收到请求后将其分发给下面的worker进程

以下是通过集群cluster创建多进程架构的简易例子:


const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) { // 主进程
    // 根据cpus/2开启子进程数
    for (let i = 0; i < require('os').cpus().length / 2; i++) {
      	// 衍生工作进程。
        createWorker();
    }

    function createWorker() {
        // 创建子进程
        var worker = cluster.fork();
	// 监听子进程发送的消息
        worker.on('message', function (msg) {
          
        });
	// 发送消息给特定工作进程
      	worker.send('server')
        // 监听退出
        worker.on('exit', function (code, signal) {
           console.log('worker process exited, code: %s signal: %s', code, signal);
        });
    }

} else {
    // 当进程出现会崩溃的错误
    process.on('uncaughtException', function (err) {
        // 这里可以做写日志的操作
        console.log(err);
        // 退出进程
        process.exit(1);
    });

    // 内存使用过多,自杀
    if (process.memoryUsage().rss > 734003200) {
        process.exit(1);
    }
  	
    // 接收主进程send方法发送的消息
    process.on('message', function (msg) {
        process.send('pid:'+process.pid);
    });

    // 工作进程可以共享任何 TCP 连接。
    // 在本例子中,共享的是 HTTP 服务器。
    http.createServer((req, res) => {
    	res.writeHead(200);
    	res.end('你好世界\n');
    }).listen(8000);

  console.log(`工作进程 ${process.pid} 已启动`);
}

cluster集群采用的是经典的主从模型,会创建个主进程(master),然后根据指定数量创建子进程(worker),由master进程负责管理所有的子进程,主进程不处理具体的任务,主要工作是负责调度和管理。cluster模块使用内置的负载均衡来处理线程之间的压力,该负载均衡使用了Round-robin算法(循环算法)。

cluster模块开启的多个子进程监听同一个端口,为啥没报错误: Error:listen EADDRIUNS 呢?

这是由于master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket句柄发送给子进程。

node.js进程守护

通常通过命令行窗口执行命令node app.js,启动一个 程序。但当命令行窗口关闭,服务会立刻断掉;或者当Node服务意外崩溃此时无法自动重启,这些情况都是不想看到的。因此需要对进程守护,出现意外情况重启。我们经常采用的是pm2。

例子:指定生产环境,启动一个名为app的node服务

pm2 start app.js --env production --name app

关于pm2其他命令,以及使用方法,详细看官网:

pm2.keymetrics.io/docs/usage/…

node.js线程

node.js是单线程指的是JavaScript的执行是单线程的,但JavaScript的宿主环境无论是Node还是浏览器都是多线程的。以下代码一行可以手动更改线程池默认数量。

process.env.UV_THREADPOOL_SIZE = 36   // 线程池默认大小为4

总结

关于线程和进程的初步浅析就到这了,如果你对浏览器的多进程架构有想了解的,可以看看下面这篇文章:

Chrome浏览器多进程架构的3个知识点