写点什么

浅谈 nodejs 进程和线程

用户头像
梁龙先森
关注
发布于: 2021 年 02 月 22 日
浅谈nodejs进程和线程

进程概念

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


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

线程概念

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


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


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


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

node.js 处理耗时操作

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

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

// forkApp.jsconst 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.jsconst 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 其他命令,以及使用方法,详细看官网:

http://pm2.keymetrics.io/docs/usage/quick-start/


node.js 线程

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

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

总结

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

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


发布于: 2021 年 02 月 22 日阅读数: 27
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
浅谈nodejs进程和线程