本篇文章通过Node.js的角度讲解进程与线程,除了介绍概念外还会讲解一些在项目中的实战的应用。

在文章开始前先问几个问题:

  1. Node.js是单线程吗?

  2. Node.js 做耗时的计算时候,如何避免阻塞?

  3. Node.js如何实现多进程的开启和关闭?

  4. Node.js可以创建线程吗?

  5. 你们开发过程中如何实现进程守护的?

  6. 除了使用第三方模块,你们自己是否封装过一个多进程架构?


一、 进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器(来自百科)。我们启动一个服务、运行一个实例,就是开一个服务进程。多进程就是进程的复制(fork),fork 出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才可数据共享。


二、 线程

线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。
同一块代码,可以根据系统CPU核心数启动多个进程,每个进程都有属于自己的独立运行空间,进程之间是不相互影响的。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage),线程又有单线程和多线程之分,具有代表性的 JavaScript、Java 语言。


1. 单线程

Javascript 就是属于单线程,程序顺序执行,可以想象一下队列,前面一个执行完之后,后面才可以执行,当你在使用单线程语言编码时切勿有过多耗时的同步操作,否则线程会造成阻塞,导致后续响应无法处理。你如果采用 Javascript 进行编码时候,请尽可能的使用异步操作。

Google 的V8 Javascript引擎已经在Chrome浏览器里证明了它的性能,所以Node.js的作者Ryan Dahl选择了v8作为Node.js的执行引擎,v8赋予Node.js高效性能的同时也注定了Node.js和大名鼎鼎的Nginx一样,都是以单线程为基础的,当然这也正是作者Ryan Dahl设计Node.js的初衷。

单线程优缺点

高性能
首先,单线程避免了频繁创建、切换进程的开销,使执行速度更加迅速。
第二,资源占用小,Node.js在大负荷下对内存占用仍然很低。

线程安全
单线程的js还保证了绝对的线程安全,不用担心同一变量同时被多个线程进行读写而造成的程序崩溃。线程安全的同时也解放了开发人员,免去了多线程编程中忘记对变量加锁或者解锁造成的悲剧。

异步和非阻塞
Node.js是单线程的,但是它如何做到I/O的异步和非阻塞的呢?其实Node.js在底层访问I/O还是多线程的,Node.js的fs模块用到libuv来处理I/O,所以在我们看来Node.js的代码就是非阻塞和异步形式的。

单线程和多核
线程是cpu调度的一个基本单位,一个cpu同时只能执行一个线程的任务,同样一个线程任务也只能在一个cpu上执行,所以如果你运行Node.js的机器是像i5,i7这样多核cpu,那么将无法充分利用多核cpu的性能来为Node.js服务。


2. 多线程

在C++、C#、python等其他语言都有与之对应的多线程编程,有些时候这很有趣,带给我们灵活的编程方式;但是也可能带给我们一堆麻烦,在编写更多代码的同时也存在着更多的风险,线程的切换和锁也会造成系统资源的开销。

多线程的代价还在于创建新的线程和执行期上下文线程的切换开销,由于每创建一个线程就会占用一定的内存,当应用程序并发大了之后,内存将会很快耗尽。


三、 Node.js线程与进程

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.env:环境变量,例如通过 process.env.NODE_ENV 获取不同环境项目配置信息
process.nextTick:这个在谈及 Event Loop 时经常为会提到
process.pid:获取当前进程id
process.ppid:当前进程对应的父进程
process.cwd():获取当前进程工作目录
process.platform:获取当前进程运行的操作系统平台
process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
进程事件:process.on('uncaughtException', cb) 捕获异常信息、
    process.on('exit', cb)进程推出监听
三个标准流:process.stdout 标准输出、process.stdin 标准输入、
    process.stderr 标准错误输


四、 Node.js进程创建

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 核心数设置。


五、 Node.js守护进程

守护进程运行在后台不受终端的影响,什么意思呢?比如当我们打开终端执行 node app.js 开启一个服务进程之后,这个终端就会一直被占用,如果关掉终端,服务就会断掉,即前台运行模式。如果采用守护进程进程方式,这个终端我执行 node app.js 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。

在实际工作中对守护进程的健壮性要求还是很高的,例如:进程的异常监听、工作进程管理调度、进程挂掉之后重启等等,这些还需要我们去不断思考。


六、 总结

单线程的Node.js给我们编码带来了太多的便利和乐趣,我们应该时刻保持清醒的头脑,在Node.js代码中任何一个隐藏的问题都可能击溃整个线上正在运行的Node.js程序。

单线程异步的Node.js不代表不会阻塞,在主线程做过多的任务可能会导致主线程的卡死,影响整个程序的性能,所以我们要非常小心的处理cpu密集型任务,合理的利用各种技术把任务丢给子线程或子进程去完成,保持Node.js主线程的畅通。

线程/进程的使用并不是没有开销的,尽可能减少创建和销毁线程/进程的次数,可以提升我们系统整体的性能和出错的概率。最后请不要一味的追求高性能和高并发,因为我们可能不需要系统具有那么大的吞吐率。高效,敏捷,低成本的开发才是项目所需要的。


参考文章: