进程与线程:
不管是进程,还是线程,都是操作系统层面的概念。应用程序最终都是跑在操作系统上面的。
进程 Process:
启动一次应用程序就是一个进程,它占有一片独有的内存空间。进程是操作系统管理应用程序的一种方式。
比如打开一个 VSCode 就启动了一个 VSCode 进程,打开两个记事本就启动了两个记事本进程。
操作系统是如何做到让多个进程(例如:边听歌、边写代码、边查资料)同时工作的呢?
如果是多核 CPU 的话,是真正可以做到并行工作的。
如果是单核 CPU 的话,是因为 CPU 的运算速度非常快,可以在多个进程之间快速地切换,当进程中的线程获取到时间片时,就可以快速地执行代码。对于用户来说是感受不到这种快速的切换的。
多进程与单进程:
多进程程序:有的程序是可以启动多个进程的,这种程序称为多进程程序。
单进程程序:有,的程序只可以启动一个进程,这种程序称为单进程程序。
线程 Thread:
在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,进程内的这些“子任务”就称为线程。线程是操作系统能够运行调度的最小单位。
比如 Word,可以同时进行打字、拼写检查、打印等事情。
多线程与单线程:
多线程程序:一个进程内可以有多个线程,这种程序称为多线程程序。多线程能有效提升 CPU 的利用率,但是会产生创建多线程开销、线程间切换开销、死锁与状态同步的问题。
单线程程序:一个进程内只可以有一个线程,这种程序称为单线程程序。单线程没有创建多线程开销、没有线程间切换开销、不会出现因为线程之间争夺资源导致的死锁与状态同步的问题,但是效率低。
进程与线程的关系:
由于每个进程至少要干一件事,所以,一个进程至少有一个线程,被称为主线程,进程启动后自动创建主线程。
多个进程之间的数据是不能直接共享的(因为进程间的内存是相互独立的);但是一个进程内的多个线程是可以共享其同一进程内的数据的。
应用程序必须运行在某个进程的某个线程上。如何调度进程和线程,完全由操作系统决定,应用程序不能自己决定。
JS 是单线程的:
目前大多数浏览器都是多进程的,每个 Tab 标签页都是一个进程,这是为了防止一个页面卡死造成所有页面都无法响应,整个浏览器都需要强制退出的情况。每个进程中又有很多的线程,但是都只有一条单独的线程是用来执行 JS 代码的。因此,JS 是一门单线程的语言。
但是,在 H5 中开启 Web Workers 后可以进行多线程运行。
因为 JS 的单线程,在同一时刻只能做一件事。因此所有任务需要排队,前一个任务结束,才会执行后一个任务。如果某个任务耗时过长,就会阻塞 JS 线程。
// 这个长时间的 for 循环会阻塞 JS 线程,直到 for 循环完成才能执行下面的代码
for (let i = 0; i < 1000000; i++) {
console.log(i)
}
console.log('Hello')
JS 为什么是单线程?
JS 作为浏览器的脚本语言,主要用来实现与用户的交互以及操作 DOM,如果 JS 是多线程的话,会带来很严重的同步任务。
例如:一个线程在一个 DOM 节点中增加内容,另一个线程要删除这个 DOM 节点,那么这个 DOM 节点究竟是要增加还是删除呢?会带来严重的同步问题。
同步任务与异步任务:
浏览器中存在很多耗时的任务的场景,例如:网路请求、事件监听、定时器等。JS 语言的设计者意识到,这时主线程完全可以挂起处于等待中的任务,先运行排在后面的任务。等到有了结果,再回过头把挂起的任务继续执行下去。
于是,将任务分为了同步任务和异步任务。
同步任务:
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
JS 中大部分都是同步任务。
function fun1() {
console.log(1)
}
function fun2() {
console.log(2)
}
fun1()
fun2()
异步任务:
异步任务是指不进入主线程,而进入任务队列的任务,只有等到主线程中的所有同步任务都执行完毕,完成了的异步任务才会进入主线程中执行。
任务队列是一个先进先出的数据结构。
// setTimeout 函数本身和传入的回调函数还是由 JavaScript 线程来执行的,但是计时操作是由浏览器中的其他线程来进行的,而不是 JavaScript 线程,因此不会阻塞 JavaScript 线程,会继续执行下面的代码
// setTimeout 函数本身的执行立即结束,但是并不会执行传入的回调函数,而是去执行后面的 console.log();由浏览器中的其他线程来进行计时,直到计时结束,浏览器会将传入的回调函数放到任务队列中,取出执行
setTimeout(() => {
console.log('计时器')
}, 10000)
console.log('World')
异步任务分为宏任务(macro-task)和微任务(micro-task)。在执行任何一个宏任务之前,都会先检查一下微任务队列中是否有任务,如果有的话,会优先执行完微任务队列中的任务才执行宏任务。
-
宏任务:setTimeout、setInterval、Ajax、DOM 事件监听。
setTimeout(()=>{ console.log(1) }, 100) console.log(2) // 最终打印 2 1
-
微任务:Promise 的 then 回调(
new Promise()
在实例化的过程中所执行的代码都是同步的,then 才是异步的)、queueMicrotask()
(插入一个微任务)。new Promise(resolve => { console.log(1) resolve() }).then(() => { console.log(2) }) console.log(3) // 最终打印 1 3 2
console.log(1) function asyncFn() { return new Promise(resolve => { console.log(2) resolve() }) } async function fn() { console.log(3) // await 会暂停异步函数的执行,直到后面的表达式有了结果 // 也就是说,直到调用 resolve(),Promise 的状态变为已完成,下一行的代码才会执行,因此 await 下一行的代码,可以看作是 Promise.then(),是微任务 await asyncFn() console.log(4) } fn() console.log(5) // 最终打印 1 3 2 5 4
console.log(1) // async 异步函数默认返回的也是一个 Promise 对象 async function asyncFn() { console.log(2) } async function fn() { console.log(3) // await 会暂停异步函数的执行,直到后面的表达式有了结果 // 也就是说,直到调用 resolve(),Promise 的状态变为已完成,下一行的代码才会执行,因此 await 下一行的代码,可以看作是 Promise.then(),是微任务 await asyncFn() console.log(4) } fn() console.log(5) // 最终打印 1 3 2 5 4
浏览器的事件循环机制(Event Loop):
浏览器的事件循环机制:
- 所有的同步任务都在主线程上执行,形成一个执行上下文栈。
- 主线程之外,还存在任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行上下文栈中的所有同步任务执行完毕,就会开始不断重复地从任务队列中读取事件,那些对应的异步任务,于是结束等待状态,依次进入执行上下文栈中开始执行。
// 同步任务
console.log('1')
setTimeout(() => { // 宏任务
console.log('2')
}, 0)
new Promise((resolve) => { // 同步任务
console.log('3')
resolve()
}).then(() => { // 微任务
console.log('4')
}) // 打印 1 3 4 2