- Node.js是单线程吗?
- Node.js 做耗时的计算时候,如何避免阻塞?
- Node.js如何实现多进程的开启和关闭?
- Node.js可以创建线程吗?
进程
进程Process是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器。进程是资源分配的最小单位。我们启动一个服务、运行一个实例,就是开一个服务进程,例如Java 里的JVM本身就是一个进程,Node.js里通过node app.js开启一个服务进程,多进程就是进程的复制(fork),fork出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了IPC通信,进程之间才可数据共享。
// Node.js开启服务进程
const http = require('http');
const server = http.createServer();
server.listen(3000,()=>{
process.title='node process';
console.log('进程id', process.pid)
线程
线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的,单线程就是一个进程只开一个线程。
Node.js中的进程与线程
Web业务开发中,如果你有高并发应用场景,那么Node.js会是你不错的选择。
在单核CPU系统上我们采用“单进程 + 单线程”的模式来开发,在多核CPU系统上,我们可以通过 child_process开启多个进程(Node.js在v0.8版本之后新增了Cluster来实现多进程架构),即“多进程 + 单线程”模式。
注意:开启多进程不是为了解决高并发,主要是解决了单进程模式下Node.js的CPU利用率不足的情况,以便能够充分利用多核CPU的性能。
1)process 模块
Node.js中的进程Process是一个全局对象,无需require便可直接使用,它给我们提供了当前进程中的一些相关信息。
部分常用到功能点:
- process.env:环境变量,例如通过process.env.NODE_ENV来获取不同环境项目配置信息。
- process.nextTick:这个在谈及到EventLoop时经常为会提到。
- process.pid:获取当前进程id。
- process.ppid:当前进程对应的父进程id。
- process.cwd():获取当前进程的工作目录。
- process.platform:获取当前进程运行的操作系统平台。
- process.uptime():当前进程已运行时间,例如:pm2守护进程的uptime值。
- 进程事件:process.on(‘uncaughtException’, cb) 捕获异常信息、process.on(‘exit’, cb)进程退出监听。
- 三个标准流:process.stdout标准输出、process.stdin标准输入、process.stderr标准错误输出。
- process.title:指定进程名称,有时候我们需要给进程指定一个名称。
2)child_process模块
child_process是Node.js的内置模块,常用函数:
- child_process.spawn():适用于返回大量数据,例如图像处理、二进制数据处理。
- child_process.exec():适用于小量数据,maxBuffer属性默认值为200 * 1024,超出这个默认值将会导致程序崩溃,数据量过大时可采用spawn。
- child_process.execFile():类似于child_process.exec(),区别是不能通过shell来执行,不支持像I/O重定向和文件查找这样的行为。
- child_process.fork():衍生新的进程,进程之间是相互独立的,每个进程都有自己的V8实例、内存,系统资源是有限的,不建议衍生太多的子进程出来,通长根据系统CPU 核心数来设置。
// fork开启子进程
const http = require('http');
const fork = require('child_process').fork;
const server = http.createServer((req, res) => {
if(req.url == '/compute'){
const compute = fork('./fork_compute.js');
compute.send('开启一个新的子进程');
// 当一个子进程使用process.send()发送消息时会触发'message'事件
compute.on('message', sum => {
res.end(`Sum is ${sum}`);
compute.kill();
});
// 子进程监听到一些错误消息退出
compute.on('close', (code, signal) => {
console.log(`收到close事件,子进程收到信号 ${signal} 而终止,退出码 ${code}`);
compute.kill();
})
}else{
res.end(`ok`);
}
});
server.listen(3000, 127.0.0.1, () => {
console.log(`server started at http://${127.0.0.1}:${3000}`);
});
// 创建子进程拆分出来单独进行运算
const computation = () => {
let sum = 0;
console.info('计算开始');
console.time('计算耗时');
for (let i = 0; i < 1e10; i++) {
sum += i
};
console.info('计算结束');
console.timeEnd('计算耗时');
return sum;
};
process.on('message', msg => {
console.log(msg, 'process.pid', process.pid); // 子进程id
const sum = computation();
// 如果Node.js进程是通过进程间通信产生的,那么process.send()方法可以用来给父进程发送消息
process.send(sum);
})
3)cluster模块
cluster模块调用fork()方法来创建子进程,该方法与child_process中的fork()是同一个方法。cluster模块采用的是经典的主从模型,Cluster会创建一个master,然后根据你指定的数量复制出多个子进程,可以使用 cluster.isMaster属性来判断当前进程是master还是worker(工作进程)。由master进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。
cluster模块使用内置的负载均衡来更好地处理线程之间的压力,该负载均衡使用了Round-robin算法(也被称之为循环算法)。当使用Round-robin调度策略时,master accepts()所有传入的连接请求,然后将相应的TCP请求处理发送给选中的工作进程(该方式仍然通过IPC来进行通信)。
如果多个Node.js进程监听同一个端口时会出现"Error: listen EADDRIUNS"的错误,而cluster模块为什么可以让多个子进程监听同一个端口呢?原因是master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket句柄发送给子进程。
// cluster 开启子进程
const http = require('http');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log('Master proces id is', process.pid);
// fork workers
for(let i = 0; i < numCPUs; ++i){
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker process died,id', worker.process.pid)
})
} else {
// Worker可以共享同一个TCP连接
// 这里是一个http服务器
http.createServer(function(req, res) {
res.writeHead(200);
res.end('hello word');
}).listen(8000);
}
4)child_process模块与cluster模块总结
无论是child_process模块还是cluster模块,都是为了解决Node.js实例在单线程运行时无法利用多核CPU的问题,核心就是父进程(即master进程)负责监听端口,接收到后将其分发给下面的worker进程。
cluster模块的一个弊端:cluster模块内部隐式地构建了TCP服务器,这对使用者确实简单和透明了许多,但是这种方式无法像使用child_process模块那样灵活,因为一个主进程只能管理一组相同的工作进程,而自行通过child_process模块来创建工作进程,一个主进程则可以控制多组进程,原因是child_process模块操作子进程时,可以隐式地创建多个TCP服务器。
Node.js单线程的误区
大家平时常说的Node.js是单线程,指的是JavaScript的执行是单线程的(开发者编写的代码运行在单线程环境中),但Javascript的宿主环境,无论是Node.js还是浏览器都是多线程的,因为libuv中是有线程池的概念存在的,libuv会通过类似线程池的实现来模拟不同操作系统的异步调用,这对开发者来说是不可见的,某些异步I/O会占用额外的线程。
Node.js中最核心的是v8引擎,在 Node.js启动后,会创建v8的实例,而这个实例是多线程的。
- 主线程:编译、执行代码。
- 编译|优化线程:在主线程执行的时候,可以优化代码。
- 分析器线程:记录分析代码运行时间,为Crankshaft优化代码执行提供依据。
- 垃圾回收的几个线程。