进程与线程在服务端研发中是一个很是重要的概念,若是您在学习的时候对这一块感到混乱或者不是太理解,能够阅读下本篇内容,本篇在介绍进程和线程的概念以外,列举了不少 Demo 但愿能从实战角度帮助您更好的去理解。html
做者简介:五月君,Nodejs Developer,热爱技术、喜欢分享的 90 后青年,公众号 “Nodejs技术栈”,Github 开源项目 https://www.nodejs.red
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操做系统结构的基础,进程是线程的容器(来自百科)。咱们启动一个服务、运行一个实例,就是开一个服务进程,例如 Java 里的 JVM 自己就是一个进程,Node.js 里经过 node app.js
开启一个服务进程,多进程就是进程的复制(fork),fork 出来的每一个进程都拥有本身的独立空间地址、数据栈,一个进程没法访问另一个进程里定义的变量、数据结构,只有创建了 IPC 通讯,进程之间才可数据共享。java
关于进程经过一个简单的 Node.js Demo 来验证下,执行如下代码
node process.js
,开启一个服务进程
// process.js const http = require('http'); http.createServer().listen(3000, () => { process.title = '测试进程 Node.js' // 进程进行命名 console.log(`process.pid: `, process.pid); // process.pid: 20279 });
如下为 Mac 系统自带的监控工具 “活动监视器” 所展现的效果,能够看到咱们刚开启的 Nodejs 进程 20279node
线程是操做系统可以进行运算调度的最小单位,首先咱们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,可是一个进程是能够拥有多个线程的。git
同一块代码,能够根据系统CPU核心数启动多个进程,每一个进程都有属于本身的独立运行空间,进程之间是不相互影响的。同一进程中的多条线程将共享该进程中的所有系统资源,如虚拟地址空间,文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack),本身的寄存器环境(register context),本身的线程本地存储(thread-local storage),线程又有单线程和多线程之分,具备表明性的 JavaScript、Java 语言。github
单线程就是一个进程只开一个线程,想象一下一个痴情的少年,对一个妹子一心一意用情专注。chrome
Javascript 就是属于单线程,程序顺序执行,能够想象一下队列,前面一个执行完以后,后面才能够执行,当你在使用单线程语言编码时切勿有过多耗时的同步操做,不然线程会形成阻塞,致使后续响应没法处理。你若是采用 Javascript 进行编码时候,请尽量的使用异步操做。shell
一个计算耗时形成线程阻塞的例子编程
先看一段例子,运行下面程序,浏览器执行 http://127.0.0.1:3000/compute 大约每次须要 15657.310ms,也就意味下次用户请求须要等待 15657.310ms,下文 Node.js 进程建立一节 将采用 child_process.fork 实现多个进程来处理。api
// compute.js const http = require('http'); const [url, port] = ['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; }; const server = http.createServer((req, res) => { if(req.url == '/compute'){ const sum = computation(); res.end(`Sum is ${sum}`); } res.end(`ok`); }); server.listen(port, url, () => { console.log(`server started at http://${url}:${port}`); });
单线程使用总结浏览器
多线程就是没有一个进程只开一个线程的限制,比如一个风流少年除了爱慕本身班的某个妹子,还在想着隔壁班的漂亮妹子。Java 就是多线程编程语言的一种,能够有效避免代码阻塞致使的后续请求没法处理。
对于多线程的说明 Java 是一个很好的例子,看如下代码示例,我将 count 定义在全局变量,若是定义在 test 方法里,又会输出什么呢?
public class TestApplication { Integer count = 0; @GetMapping("/test") public Integer Test() { count += 1; return count; } public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } }
运行结果,每次执行都会修改count值,因此,多线程中任何一个变量均可以被任何一个线程所修改。
1 # 第一次执行 2 # 第二次执行 3 # 第三次执行
我如今对上述代码作下修改将 count 定义在 test 方法里
public class TestApplication { @GetMapping("/test") public Integer Test() { Integer count = 0; // 改变定义位置 count += 1; return count; } public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } }
运行结果以下所示,每次都是 1,由于每一个线程都拥有了本身的执行栈
1 # 第一次执行 1 # 第二次执行 1 # 第三次执行
多线程使用总结
多线程的代价还在于建立新的线程和执行期上下文线程的切换开销,因为每建立一个线程就会占用必定的内存,当应用程序并发大了以后,内存将会很快耗尽。相似于上面单线程模型中例举的例子,须要必定的计算会形成当前线程阻塞的,仍是推荐使用多线程来处理,关于线程与进程的理解推荐阅读下 阮一峰:进程与线程的一个简单解释。
Node.js 是 Javascript 在服务端的运行环境,构建在 chrome 的 V8 引擎之上,基于事件驱动、非阻塞I/O模型,充分利用操做系统提供的异步 I/O 进行多任务的执行,适合于 I/O 密集型的应用场景,由于异步,程序无需阻塞等待结果返回,而是基于回调通知的机制,本来同步模式等待的时间,则能够用来处理其它任务,在 Web 服务器方面,著名的 Nginx 也是采用此模式(事件驱动),Nginx 采用 C 语言进行编写,主要用来作高性能的 Web 服务器,不适合作业务。Web业务开发中,若是你有高并发应用场景那么 Node.js 会是你不错的选择。
在单核 CPU 系统之上咱们采用 单进程 + 单线程
的模式来开发。在多核 CPU 系统之上,能够经过 child_process.fork 开启多个进程(Node.js 在 v0.8 版本以后新增了Cluster 来实现多进程架构) ,即 多进程 + 单线程
模式。注意:开启多进程不是为了解决高并发,主要是解决了单进程模式下 Node.js CPU 利用率不足的状况,充分利用多核 CPU 的性能。
Node.js 中的进程 Process 是一个全局对象,无需 require 直接使用,给咱们提供了当前进程中的相关信息。官方文档提供了详细的说明,感兴趣的能够亲自实践下 Process 文档。
以上仅列举了部分经常使用到功能点,除了 Process 以外 Node.js 还提供了 child_process 模块用来对子进程进行操做,在下文 Nodejs进程建立一节 会讲述。
关于 Node.js 进程的几点总结
Node.js 提供了 child_process 内置模块,用于建立子进程,更多详细信息可参考 Node.js 中文网 child_process
child_process.spawn()
:适用于返回大量数据,例如图像处理,二进制数据处理。child_process.exec()
:适用于小量数据,maxBuffer 默认值为 200 * 1024 超出这个默认值将会致使程序崩溃,数据量过大可采用 spawn。child_process.execFile()
:相似 child_process.exec(),区别是不能经过 shell 来执行,不支持像 I/O 重定向和文件查找这样的行为child_process.fork()
: 衍生新的进程,进程之间是相互独立的,每一个进程都有本身的 V8 实例、内存,系统资源是有限的,不建议衍生太多的子进程出来,通长根据系统 CPU 核心数设置。方式一:spawn
child_process.spawn(command, args)
建立父子进程间通讯的三种方式:
child.stdout.pipe(process.stdout);
const spawn = require('child_process').spawn; const child = spawn('ls', ['-l'], { cwd: '/usr' }) // cwd 指定子进程的工做目录,默认当前目录 child.stdout.pipe(process.stdout); console.log(process.pid, child.pid); // 主进程id3243 子进程3244
方式二:exec
const exec = require('child_process').exec; exec(`node -v`, (error, stdout, stderr) => { console.log({ error, stdout, stderr }) // { error: null, stdout: 'v8.5.0\n', stderr: '' } })
方式三:execFile
const execFile = require('child_process').execFile; execFile(`node`, ['-v'], (error, stdout, stderr) => { console.log({ error, stdout, stderr }) // { error: null, stdout: 'v8.5.0\n', stderr: '' } })
方式四:fork
const fork = require('child_process').fork; fork('./worker.js'); // fork 一个新的子进程
上文单线程一节 例子中,当 CPU 计算密度大的状况程序会形成阻塞致使后续请求须要等待,下面采用 child_process.fork 方法,在进行 cpmpute 计算时建立子进程,子进程计算完成经过 send 方法将结果发送给主进程,主进程经过 message 监听到信息后处理并退出。
fork_app.js
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}`); });
fork_compute.js
针对 上文单线程一节 的例子须要进行计算的部分拆分出来单独进行运算。
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); })
多进程架构解决了单进程、单线程没法充分利用系统多核 CPU 的问题,经过上文对 Node.js 进程有了初步的了解,本节经过一个 Demo 来展现如何启动一批 Node.js 进程来提供服务。
master.js 主要处理如下逻辑:
// master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); const server = require('net').createServer(); server.listen(3000); process.title = 'node-master' const workers = {}; const createWorker = () => { const worker = fork('worker.js') worker.on('message', function (message) { if (message.act === 'suicide') { createWorker(); } }) worker.on('exit', function(code, signal) { console.log('worker process exited, code: %s signal: %s', code, signal); delete workers[worker.pid]; }); worker.send('server', server); workers[worker.pid] = worker; console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); } for (let i=0; i<cpus.length; i++) { createWorker(); } process.once('SIGINT', close.bind(this, 'SIGINT')); // kill(2) Ctrl-C process.once('SIGQUIT', close.bind(this, 'SIGQUIT')); // kill(3) Ctrl-\ process.once('SIGTERM', close.bind(this, 'SIGTERM')); // kill(15) default process.once('exit', close.bind(this)); function close (code) { console.log('进程退出!', code); if (code !== 0) { for (let pid in workers) { console.log('master process exited, kill worker pid: ', pid); workers[pid].kill('SIGINT'); } } process.exit(0); }
工做进程
worker.js 子进程处理逻辑以下:
// worker.js const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plan' }); res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); throw new Error('worker process exception!'); // 测试异常进程退出、重建 }); let worker; process.title = 'node-worker' process.on('message', function (message, sendHandle) { if (message === 'server') { worker = sendHandle; worker.on('connection', function(socket) { server.emit('connection', socket); }); } }); process.on('uncaughtException', function (err) { console.log(err); process.send({act: 'suicide'}); worker.close(function () { process.exit(1); }) })
测试
控制台执行 node master.js 能够看到已成功建立了四个工做进程
$ node master worker process created, pid: 19280 ppid: 19279 worker process created, pid: 19281 ppid: 19279 worker process created, pid: 19282 ppid: 19279 worker process created, pid: 19283 ppid: 19279
打开活动监视器查看咱们的进程状况,因为在建立进程时对进程进行了命名,很清楚的看到一个主进程对应多个子进程。
以上 Demo 简单的介绍了多进程建立、异常监听、重启等,可是作为企业级应用程序咱们还须要考虑的更完善,例如:进程的重启次数限制、与守护进程结合、多进程模式下定时任务处理等,感兴趣的同窗推荐看下阿里 Egg.js 多进程模式
关于守护进程,是什么、为何、怎么编写?本节将解密这些疑点
守护进程运行在后台不受终端的影响,什么意思呢?Node.js 开发的同窗们可能熟悉,当咱们打开终端执行 node app.js
开启一个服务进程以后,这个终端就会一直被占用,若是关掉终端,服务就会断掉,即前台运行模式。若是采用守护进程进程方式,这个终端我执行 node app.js
开启一个服务进程以后,我还能够在这个终端上作些别的事情,且不会相互影响。
建立步骤
Node.js 编写守护进程 Demo 展现
index.js 文件里的处理逻辑使用 spawn 建立子进程完成了上面的第一步操做。设置 options.detached 为 true 可使子进程在父进程退出后继续运行(系统层会调用 setsid 方法),参考 options_detached,这是第二步操做。options.cwd 指定当前子进程工做目录若不作设置默认继承当前工做目录,这是第三步操做。运行 daemon.unref() 退出父进程,参考 options.stdio,这是第四步操做。
// index.js const spawn = require('child_process').spawn; function startDaemon() { const daemon = spawn('node', ['daemon.js'], { cwd: '/usr', detached : true, stdio: 'ignore', }); console.log('守护进程开启 父进程 pid: %s, 守护进程 pid: %s', process.pid, daemon.pid); daemon.unref(); } startDaemon()
daemon.js 文件里处理逻辑开启一个定时器每 10 秒执行一次,使得这个资源不会退出,同时写入日志到子进程当前工做目录下
// /usr/daemon.js const fs = require('fs'); const { Console } = require('console'); // custom simple logger const logger = new Console(fs.createWriteStream('./stdout.log'), fs.createWriteStream('./stderr.log')); setInterval(function() { logger.log('daemon pid: ', process.pid, ', ppid: ', process.ppid); }, 1000 * 10);
运行测试
$ node index.js 守护进程开启 父进程 pid: 47608, 守护进程 pid: 47609
打开活动监视器查看,目前只有一个进程 47609,这就是咱们须要进行守护的进程
守护进程阅读推荐
守护进程总结
在实际工做中对于守护进程并不陌生,例如 PM二、Egg-Cluster 等,以上只是一个简单的 Demo 对守护进程作了一个说明,在实际工做中对守护进程的健壮性要求仍是很高的,例如:进程的异常监听、工做进程管理调度、进程挂掉以后重启等等,这些还须要咱们去不断思考。