浏览器多进程?js单线程?

前言

之前面试的时候被问过几次这个问题。虽然都能答出来但也只是做了很简短的描述而已,而且这两部分我都是分开讲的,因为目前为止对浏览器多进程和js单线程这两个知识点的理解都是分开的,并没有将这两个东西结合起来进行理解,所以讲得是头头是道,但是自己的理解还是不太到位。今天就结合一些网上的总结,结合二者的关系再深入一点理解浏览器的机制。

区分进程和线程

来个比喻:

1
2
3
4
5
- 进程是一个工厂,工程有他独立的资源
- 工厂之间相互独立
- 线程是工厂中的工人,多个工人写作完成任务
- 工厂内有一个或多个工人
- 工人之间共享空间

再来完善下概念:

1
2
3
4
5
- 工厂的资源  ->系统分配的内存
- 工厂之间相互独立 ->进程之间相互独立
- 多个工人协作完成任务 ->多个线程在进程中协作完成任务
- 工厂内有一个或多个工人 ->一个进程由一个或多个线程组成
- 工人之间共享空间 ->同意线程下各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)

查看windows的任务管理器如下:

image.png

总结:

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次运行单位,一个进程中可以有多个线程)。
  • 不同进程之间也可以通信,不过代价较大。
  • 一般的单线程与多线程,其范围都是限定在一个进程内的。

chrome浏览器为什么要使用多进程架构

与现在很多多线程浏览器不同,chrome浏览器使用多个进程来隔离不同的网页,因此在chrome中打开一个网页相当于起了一个新进程。

在浏览器刚刚设计出来的时候,那时的网页非常简单,每个网页非常简单,每个网页的资源占有率非常低,因此一个进程处理多个网页是可行的。然而现在,大量网页变得日益复杂,把所有网页都放在一个进程的浏览器面临在健壮性、响应速度、安全性方面的挑战。因为如果浏览器中一个tab页面崩溃,会导致其他打开的网页应用也随之崩溃,显然这是无法接受的。另外相对于线程,进程之间不共享资源和地址空间,所以不会存在太多安全问题,而由于多个线程共享着地址空间和资源,所以会存在线程之间可能会恶意修改或者获取非法授权数据等复杂的安全问题

浏览器包含也那些进程

Browser进程

浏览器进程也是浏览器的主进程,主要负责协调、主控,有且只有一个,作用如下:

  • 负责浏览器界面显示,用户交互(如前进、后退等)。
  • 负责各个页面的管理,创建与销毁其他进程。
  • 将Render进程得到的内存中的Bitmap,绘制到用户界面上。
  • 网络资源的管理、下载等。

插件进程

每种类型的插件对应一个进程,仅当使用该插件时才创建。

GPU进程

GPU(graphics processing unit)最多一个,用于3D绘制等。

Render进程(渲染进程,也是浏览器内核)

Render进程,内部是多线程的,默认每个tab页面一个进程,互 不影响。主要作用为:页面渲染,脚本执行,事件处理等。

查看浏览器进程

在chrome中按shift+esc调出浏览器的任务管理器,如下:

image.png

浏览器多进程的优势

相比于单进程浏览器,多进程浏览器有如下优点:

  • 避免单个页面崩溃影响整个浏览器。
  • 避免第三方插件崩溃影响整个浏览器。
  • 多进程能充分利用多核优势。
  • 方便使用沙盒模型隔离插件等,提高浏览器的稳定性。

当然内存等资源消耗也会随之增大,有点空间换时间的意思。

浏览器内核(Render 进程)

可以这么理解,页面渲染,js执行,事件循环都是在渲染进程内进行的。浏览器的渲染进程是多线程的。渲染进程包括了下面的线程:

GUI渲染线程

  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
  • 当界面需要重绘(Repaint)或者由于某种操作引发回流(Reflow)时,该线程就会执行。
  • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程就会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行

JS引擎线程

也称JS内核,负责处理javascript脚本程序。(例如V8引擎)

  • JS引擎县城负责解析javascript脚本,运行代码。
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(Render进程)中无论什么时候都只有一个JS线程在运行JS程序。
  • JS引擎线程与GUI渲染线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面渲染的不连贯,导致页面渲染加载阻塞。

事件触发线程

  • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开县城协助)。
  • 当JS引擎执行代码块,如setTimeout时(也可以是来自浏览器内核的其他线程,如鼠标点击,AJAX异步请求等),会将对应的任务添加到事件线程中。
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到带处理队列的队尾,等待JS引擎的处理。
  • 由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会执行)。

定时器触发线程

  • setTimeoutsetInterval所在线程。
  • 浏览器定时计数器并不是由Javascript引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确性。
  • 因此通过单独的线程来计时并触发定时,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
  • W3在HTML标准中规定,要求setTimeout中低于4ms的时间间隔算为4ms

异步http请求线程

  • XMLHttpRequest在连接后是通过浏览器新开一个线程请求。
  • 当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调在放入事件队列中,再由Javascript引擎执行。

image.png

浏览器内核(Render进程)中线程之间的关系

GUI渲染线程与JS引擎线程互斥

由于javascript是可操作DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥关系,当JS引擎执行时,GUI线程会被挂起,GUI更新则会保存在一个队列中等到JS引擎线程空闲时立即被执行。

JS阻塞页面加载

从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面加载。例如:假设JS引擎正在进行耗时的运算,此时就算有GUI更新,也会被保存到队列中,等待JS引擎空闲后执行。若耗时操作所需要的时间过长,则会影响用户体验。所以一般引入外部js文件,要放在</body>之前,保证dom渲染完毕之后再加载该资源文件

webworkers

h5中增加了webworker。MDN的官方解释:

Web Workers 使得一个Web应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被阻塞/放慢。

一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 - 这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window. 因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误。

可以这样理解:

  • 创建workers时,JS引擎向浏览器申请开一个子线程,子线程是浏览器开的,完全受主线程控制,而且不能操作DOM。(workers只是某个Render进程中的JS线程的子线程
  • JS引擎线程与workers线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的顺序)。
  • workers可以理解为师浏览器给JS引擎开的外挂,用来进行大量运算,等到结果出来再将结果通信给JS引擎主线程即可。

webworkers与sharedworker

  • webworkers只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享。所以chrome在Render进程中(每一个Tab也就是一个Render 进程)创建一个新的线程来运行workers中的javascript程序
  • sharedworkders是浏览器所有页面共享的,不能采用与workers同样的方式实现,因为他不隶属于某个Render进程,可以为多个Render进程共享使用。所以chrome浏览器为sharedworker单独创建一个进程来运行javascript程序,在浏览器中每个相同的javascript只存在一个sharedworker进程,不管他被创建了多少次。
  • 总而言之:sharedworker由独立进程管理,webworkers只属于Render进程下的一个线程。

Reference

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

浏览器进程?线程?傻傻分不清楚!

为什么浏览器会使用多进程架构。