最近正值秋招,面试了很多前端同学,感悟颇多,后面我也会在公众号为大家分享下我作为面试官的一些心得,以及对于我经常会问的一些问题的讲解。
今天我们来聊一下浏览器(以Chrome
为例)对线程和进程的调度,这个问题几乎是我每次面试必问的。相信大家都看过很多面经会讲 JavaScript
的执行机制,很多同学热衷于去背这些面经,以至于连 JavaScript
是单线程的都不知道,就开始回答宏任务、微任务了... 这种我真的特别无语,是真的理解还是背出来的解题思路其实一看便知了。所以我建议大家无论是准备面试还是平时积累知识,一定不要太浮躁,要从根本上理解这个问题,而不是去记这些解题思路。
首先我们来回顾下线程和进程的概念:
CPU
进行资源分配的基本单位CPU
调度的最小单位这是进程和线程最官方也是最常见的两个定义,但是这两个概念太抽象了,很难以理解。通俗一点讲:进程可以描述为一个应用程序的执行程序,线程则是进程内部用来执行某个部分的程序。
下面再引用一段知乎的高赞回答,我感觉非常有意思:
做个简单的比喻:进程=火车,线程=车厢
当一个应用程序启动时,一个进程就被创建了。应用程序可能会创建一些线程帮助它完成某些工作,但这不是必须的。操作系统会划分出一部分内存给这个进程,当前应用程序的所有状态都将保存在这个私有的内存空间中。
当你关闭应用时,进程也就自动蒸发掉了,操作系统会将先前被占用的内存空间释放掉。
一个程序并不一定只有一个进程,进程可以让操作系统再另起一个进程去处理不同的任务。当这种情况发生时,新的进程又将占据一块内存空间。当两个进程需要通信时,它们进行进程间通讯。
许多应用程序都被设计成以这种方式进行工作,所以当其中一个进程挂掉时,它可以在其他进程仍然运行的时候直接重启。
理解了上面的内容,我们再来重新梳理多进程和多线程的概念:
由于浏览器本身没有统一的规范,不同的浏览器之间的架构可能完全不同,在浏览器刚被设计出来的时候,那时的网页非常的简单,每个网页的资源占有率是非常低的,因此一个进程处理多个网页时可行的。然后在今天,大量网页变得日益复杂。把所有网页都放进一个进程的浏览器面临在健壮性,响应速度,安全性方面的挑战,所以大部分现代浏览器都是多进程的。
从上面的图我们可以很明显的看出 Chrome
是一个多进程的架构,我们打开一个浏览器时会启动多个不同的进程协助浏览器将页面为我们呈现出来:
浏览器最核心的进程,负责管理各个标签页的创建和销毁、页面显示和功能(前进,后退,收藏等)、网络资源的管理,下载等。
负责每个第三方插件的使用,每个第三方插件使用时候都会创建一个对应的进程、这可以避免第三方插件crash影响整个浏览器、也方便使用沙盒模型隔离插件进程,提高浏览器稳定性。
负责3D绘制和硬件加速
浏览器会为每个窗口分配一个渲染进程、也就是我们常说的浏览器内核,这可以避免单个 page crash
影响整个浏览器。
浏览器内核就是浏览器渲染进程,从接收下载文件后再到呈现整个页面的过程,由浏览器渲染进程负责。浏览器内核是多线程的,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
GUI
渲染线程http
请求线程JavaScript
引擎线程GUI
渲染线程负责渲染浏览器界面 HTML
元素,当界面需要重绘(Repaint
)或由于某种操作引发回流(reflow
)时,该线程就会执行。
浏览器定时计数器并不是由 JavaScript
引擎计数的, 因为 JavaScript
引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。
Javascript
引擎,也可以称为JS内核,主要负责处理 Javascript
脚本程序,例如V8引擎。Javascript
引擎线程理所当然是负责解析 Javascript
脚本,运行代码。
由于 JavaScript
是可操纵 DOM
的,如果在修改这些元素属性同时渲染界面(即 JavaScript
线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI
渲染线程与 JavaScript
引擎为互斥的关系,当 JavaScript
引擎执行时 GUI
线程会被挂起, GUI
更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
从上面我们了解到 JavaScript
的执行是单线程的,也就是说,同一个时间只能做一件事。那么,为什么 JavaScript
不设计成多个线程呢?这样不是效率更高?
作为浏览器脚本语言, JavaScript
的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript
同时有两个线程,一个线程在某个 DOM
节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生, JavaScript
就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
那么既然 JavaScript
本身被设计为单线程,为何还会有像 WebWorker
这样的多线程 API
呢?我们来看一下 WebWorker
的核心特点就明白了:
Worker
时, JS
引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)JS
引擎线程与 worker
线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)所以 WebWorker
并不违背 JS引擎是单线程的
这一初衷,其主要用途是用来减轻cpu密集型计算类逻辑的负担。
好了,了解完以上知识,再去学习 JavaScript
的执行机制吧,这些知识会让你更快深入的理解。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8