进程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)
线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的,单线程就是一个进程只开一个线程。
Web业务开发中,如果你有高并发应用场景,那么Node.js会是你不错的选择。
在单核CPU系统上我们采用“单进程 + 单线程”的模式来开发,在多核CPU系统上,我们可以通过 child_process开启多个进程(Node.js在v0.8版本之后新增了Cluster来实现多进程架构),即“多进程 + 单线程”模式。
注意:开启多进程不是为了解决高并发,主要是解决了单进程模式下Node.js的CPU利用率不足的情况,以便能够充分利用多核CPU的性能。
1)process 模块
Node.js中的进程Process是一个全局对象,无需require便可直接使用,它给我们提供了当前进程中的一些相关信息。
部分常用到功能点:
2)child_process模块
child_process是Node.js的内置模块,常用函数:
// 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是单线程,指的是JavaScript的执行是单线程的(开发者编写的代码运行在单线程环境中),但Javascript的宿主环境,无论是Node.js还是浏览器都是多线程的,因为libuv中是有线程池的概念存在的,libuv会通过类似线程池的实现来模拟不同操作系统的异步调用,这对开发者来说是不可见的,某些异步I/O会占用额外的线程。
Node.js中最核心的是v8引擎,在 Node.js启动后,会创建v8的实例,而这个实例是多线程的。