Node的进程和线程

122 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第25天,点击查看活动详情

Node.js的进程

什么是进程?

进程是操作系统分配资源的最小单位

但这不是定义,真正的定义是:进程是应用程序的执行副本

启动应用程序:应用程序(磁盘) --> 进程(内存中)

在这个过程中,操作系统要给进程分配资源,这些资源包括:

  • 用户和组(哪个用户、组启动的进程)
  • 目录资源
    • 进程的工作目录
    • 进程的可见目录
  • 文件资源
    • 设备资源
    • 网络资源
    • 数据文件
    • 代码文件
    • …………
  • 主线程

注意:进程不能执行程序!---执行程序的是现场。操作系统会为每个进程分配一个主线程,然后进程还可以分配其他线程。

NodeJS的进程也是进程,进程下可以有很多线程。进程是Node引擎的执行副本,是操作系统分配资源的最小单位。Node下有很多线程一起完成执行程序的工作

  • Node进程也有主线程
  • Node进程也可以使用多线程(只不过不允许用户用)
  • Node进程有工作目录
  • Node进程可以拥有文件资源

总之,操作系统给进程的,Node进程也都能享受,操作系统不给进程的Node进程也没有。从这个层面看,Node进程、Java进程、Python进程没有本质区别

Node.js的线程模型

进程是分配资源的,线程是执行程序的。

为什么,不既分配资源,也执行程序,统一线程和进程?

因为进程需要的资源多,线程需要的资源少。线程只需要执行程序相关的资源:

  • 程序计数器(执行到哪一行)
  • 栈(存储执行程序的中间结果)
  • 寄存器(辅助计算以及控制)

形象的说,创建进程是秒级的,创建线程是ms级的。

所以,Node.js是单进程吗?

没有单进程这个说法,只有单线程。进程作为应用的执行副本,本身就是一个完整实体,也没有多进程这种说法。

Node.js是单线程吗?

Node.js进程不是单线程的,内部有很多线程。用户写的Node.js程序值在一个线程中执行。

线程的本质是抽象要并发做的事情,比如说:

  • 浏览器渲染
  • 用户操作页面
  • 发送网络请求

这3件事情,用3个线程来表达,看上去这3件事情就可以同时被完成,这就是并行的能力。

Node.js每遇到一件需要操作系统支持的事情,就会使用一个单独的线程。比如说:

  • 读取文件
  • 发送网络请求
  • 定时器
  • ……

为了防止线程太多,Node.js内部有一个线程池,用于处理需要OS支持的行为。

对于用户的程序,Node.js也用一个单独的线程。

那么Node.js为什么不让单线程执行所有程序呢? ---因为它需要并发

方案:基于事件循环

image-20210803021534672.png

浏览器是这样的吗? —— 当然是一样的工作原理。

image-20210803021631449.png

所以核心是:Event Loop。 Event Loop是什么?

  • Event Loop是一个单线程程序
  • Event Loop是驱动javascript程序执行的动力源

将下面的程序带入Event Loop帮助你思考:

  • setTimeout
  • requestAnimationFrame
  • setInterval
  • new Promise(...)

消息循环什么时候获得控制权? --用户线程放弃控制权的时候。举个例子:

// Helloworld.ts
setTimeout(() => {
    //....
}, 1000)

上面的程序在第0秒的时候进入消息循环

Event Loop 也没有事情可做了怎么办?

  • 如果消息循环上所有注册的任务都处理完了(切换回用户程序)
  • 用户程序如果没有事情做,那么就结束执行

我们通常将用户程序称为:主栈(Main Stack)上的程序。本质上,消息循环和用户程序在同一个线程中执行,因此也发生在Main Stack上。

那么定时器、网络请求、数据库操作、文件操作等等行为,发生在哪里?

  • 发生在线程池上

所以,这张图到底在说啥?

image-20210803020741121.png

图中是Node.js消息循环中对任务优先级的控制。

先问一个问题,用户定时器的优先级高,还是读取文件回调函数的优先级高?

  • setTimeout vs fs.readFile(...., callback)

可不可以先到先得(这个叫做朴素的公平性),结果就是:定时器会更加不稳定。

另一方面,从操作系统接收到网络请求、文件读取有了结果的优先级,和回调函数相比,谁更高?

  • fs.readFile(...., callback)
  • 文件读取成功的消息

同学可能问:上面不是一个东西吗?——

  • 前者是一个callback函数
  • 后者是操作系统给消息循环的通知

当然是前者高,这是因为前者代表一次读取完的请求,后者代表一次读取完的请求。

浏览器中也是这样吗?

当然,只要是V8引擎,都是类似的设计。

image-20210803023527636.png

浏览器中用户程序(Main Stack)先执行,Main Stack没有要执行的内容了,进入消息循环。有宏任务先执行宏任务,任何一个宏任务执行完都检查有没有微任务。

总体来说:宏任务是由宿主发起的,而微任务由JavaScript自身发起。Node.js为什么不沿用宏任务、微任务而使用多级队列?

这是因为Node.js并不是在处理用户交互(UI)。用户交互程序的特点就是用户对等待0容忍。因此一个宏任务,比如一次鼠标点击(onClick),应该被马上执行。如果一个宏任务被响应,其中如果有Promise也应该尽快响应。Node.js不需要这样,因此很大的一个区别就是:

  • Node.js总是执行完一级队列再执行下一级
  • 浏览器执行一个宏任务然后执行所有的微任务

划重点:原理实现一致(V8)的机制,实现根据不同场景有差异。

总结

Node.js不是一个单线程模型,从设计上它的用户程序和EventLoop在一个线程上执行——因此也在一个CPU核心上执行。因为,一个线程同一时刻只在一个CPU上执行。在Node.js进程被创建到销毁的这段时间,EventLoop将一直存在,因此这个线程会在一个CPU上完成所有的工作。那么,其他线程,比如文件系统访问、网络请求也是在一个CPU上吗? 当然可以不是,它们可以用线程池技术利用多核的特性。

所以Node.js是单线程吗? —— 和面试官要说清楚。 你说是不是单线程都可以,毕竟用户程序+EventLoop是在单线程执行的。 但是Node.js进程内部是有多线程模型的,之所以用户程序+EventLoop单线程,是因为在非计算密集型(也就是I/O密集型)的工作场景下,这样效率最高。

另外,setTimeout和setImmediate哪个优先? ——这不重要。 你在写程序中开始思考这个问题,说明程序写错了。 面试官问,你告诉它这不重要。对程序员来说,它们都是异步,异步就不要对他们的执行顺序做任何假设

管他是宏任务、微任务、callback还是timer,通通异步转同步(promise,yield用起来)。

把更多的时间花在更有价值的思考上,多问自己这几个问题:

  • 还认为JS是单线程吗?JS进程内有线程池吗?
  • 任务的本质是协程(或者线程吗)?
  • 为什么不用多线程设计Node.js?
  • 异步转同步(yield,async)性能下降吗?