进程与线程
进程与线程:
- 进程是 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 引擎中这个规则就是事件循环机制。
常见的阻塞:
alert
、confirm
、prompt
两种任务队列
宏任务(macrotask)队列:
script
(整体代码)setTimeout
、setInterval
requestAnimationFrame
- I/O 操作
- UI 渲染、交互事件
setImmediate
(Node.js 环境)
微任务(microtask)队列:
Promise.then
- Observer(如:
MutationObserver
、IntersectionObserver
、PerformanceObserver) process.nextTick
(Node.js 环境)
宏任务队列可以有多个,微任务队列只有一个
为什么要引入微任务的概念,只有宏任务可以吗?
宏任务:先进先出的执行原则
当有高优先级的任务时无法“插队”,所以引入微任务:每个宏任务结束后,都要清空所有的微任务。
浏览器
- 执行宏任务队列队头的任务
- 将微任务中的所有任务依次执行,清空微任务队列
- 执行渲染操作,更新界面
- 处理 worker 相关的任务
- 重复第一步,如此循环(如果没有任务则等待)
浏览器中的宏任务队列有多个,如:定时器任务队列、网络请求任务队列等,浏览器将根据自己的算法(考虑队列优先级、搁置时间等因素)决定下一个宏任务从哪个队列中取
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 │
└───────────────────────────┘
- timers(定时器):
setTimeout()
和setInterval()
安排的回调 - pending callbacks(挂起的回调):执行推迟到下一个循环迭代的 I/O 回调
- idle, prepare(空闲,准备):仅系统内部使用
- poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(除了关闭、计时器、
setImmediate()
之外的几乎所有回调),node 会在适当的时候阻塞在这里 - check(检查):
setImmediate()
的回调 - close callbacks(关闭的回调函数):一些准备关闭的回调函数,如:
socket.on('close', ...)
Node.js V10 及以前:
- 执行完一个阶段中的所有任务
- 执行完 nextTick 队列里的内容
- 执行完微任务队列的内容
- 重复第一步,如此循环(如果没有任务则结束 JS)
Node.js V11 及以后:和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列
总结
- 三个空间:执行栈、宏任务队列(可能多个)、微任务队列
- 第一次是 script(整体代码)进入执行栈,当在执行过程中遇到异步任务时,会交给浏览器的其他模块进行处理,当异步任务完成(如:计时器结束、网络请求响应返回,Promise 完成),task(回调函数)会放入到任务队列(宏任务队列或微任务队列)之中
- 当执行栈代码(同步任务)执行完毕后,依次执行当前微任务队列中的所有任务,之后将宏任务队列的队头任务压入执行栈执行
- 重复步骤 3,直到无任务,结束 JS 或等待