进程与线程

进程与线程:

  • 进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位
  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  • 一个进程的内存空间是共享的,每个线程都可用这些共享内存

多进程与多线程:

  • 多进程:在同一个时间内,计算机允许多个进程处于运行状态。如:听歌的时候聊 QQ,互不影响
  • 多线程:程序中包含多个执行流,即:在一个程序中可以同时运行多个不同的线程来执行不同的任务,允许单个程序创建多个并行执行的线程来完成各自的任务

并行与并发:

  • 并行:宏观上来看,多个程序可以同时运行,但实际是 CPU 执行权限快速切换,其实同一时间只有一个程序在运行,其他程序在阻塞(等待 CPU 的执行权限),但由于切换太快,无法感知
  • 并发:真正的多个程序同时运行,得益于多核 CPU(硬件的支持)

事件循环

浏览器 JavaScript 引擎的两大特点:单线程非阻塞(通过 Event Loop 事件循环机制实现)。

为什么 JavaScript 是单线程?

作为浏览器脚本语言,JavaScript 可以操作 DOM,多线程同时操作 DOM 可能会出现线程安全问题(多个线程竞争一个资源——DOM)

Web Worker 说明

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

非阻塞

当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)时,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调,浏览器 JavaScript 引擎中这个规则就是事件循环机制。

常见的阻塞:alertconfirmprompt

两种任务队列

宏任务(macrotask)队列:

  • script(整体代码)
  • setTimeoutsetInterval
  • requestAnimationFrame
  • I/O 操作
  • UI 渲染、交互事件
  • setImmediate(Node.js 环境)

微任务(microtask)队列:

  • Promise.then
  • Observer(如:MutationObserverIntersectionObserver、PerformanceObserver)
  • process.nextTick(Node.js 环境)

宏任务队列可以有多个,微任务队列只有一个

为什么要引入微任务的概念,只有宏任务可以吗?

宏任务:先进先出的执行原则

当有高优先级的任务时无法“插队”,所以引入微任务:每个宏任务结束后,都要清空所有的微任务。

浏览器

  1. 执行宏任务队列队头的任务
  2. 将微任务中的所有任务依次执行,清空微任务队列
  3. 执行渲染操作,更新界面
  4. 处理 worker 相关的任务
  5. 重复第一步,如此循环(如果没有任务则等待)

浏览器中的宏任务队列有多个,如:定时器任务队列、网络请求任务队列等,浏览器将根据自己的算法(考虑队列优先级、搁置时间等因素)决定下一个宏任务从哪个队列中取

Node.js

Node.js 采用 V8 作为 JS 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是其中的实现

libuv 中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
  1. timers(定时器):setTimeout()setInterval() 安排的回调
  2. pending callbacks(挂起的回调):执行推迟到下一个循环迭代的 I/O 回调
  3. idle, prepare(空闲,准备):仅系统内部使用
  4. poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(除了关闭、计时器、setImmediate() 之外的几乎所有回调),node 会在适当的时候阻塞在这里
  5. check(检查):setImmediate() 的回调
  6. close callbacks(关闭的回调函数):一些准备关闭的回调函数,如:socket.on('close', ...)

Node.js V10 及以前:

  1. 执行完一个阶段中的所有任务
  2. 执行完 nextTick 队列里的内容
  3. 执行完微任务队列的内容
  4. 重复第一步,如此循环(如果没有任务则结束 JS)

Node.js V11 及以后:和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列

总结

  1. 三个空间:执行栈、宏任务队列(可能多个)、微任务队列
  2. 第一次是 script(整体代码)进入执行栈,当在执行过程中遇到异步任务时,会交给浏览器的其他模块进行处理,当异步任务完成(如:计时器结束、网络请求响应返回,Promise 完成),task(回调函数)会放入到任务队列(宏任务队列或微任务队列)之中
  3. 当执行栈代码(同步任务)执行完毕后,依次执行当前微任务队列中的所有任务,之后将宏任务队列的队头任务压入执行栈执行
  4. 重复步骤 3,直到无任务,结束 JS 或等待