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