多进程和 JS 单线程

参考文档

进程 VS 线程

线程是不能单独存在的,它是由进程来启动和管理。一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。

进程和线程的关系有如下几个特点:

  • 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
  • 线程之间共享进程中的数据。
  • 当一个进程关闭之后,操作系统会回收进程所占用的内存。
  • 进程之间的内容相互隔离。

进程内使用多线程并行处理,可以大大提升性能。

作者注

采用类比的方式,可以将进程理解为一个家庭,同一个户口本上的所有人组成了这个家庭,家庭里的每个人都是一个线程,而户口本上的户主就是主线程。

早期浏览器是单进程的

在 2007 年之前,市面上浏览器都是单进程的。单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。单进程浏览器存在以下问题:

  • 不稳定:插件、渲染引擎等的崩溃会引起整个浏览器的崩溃。
  • 不流畅:渲染引擎、JavaScript 执行环境、插件运行在同一个线程中,意味着同一时刻只能有一个模块可以执行,当一个模块长时间运行时,其他模块就无法运行。
  • 不安全

详情请见:浏览器工作原理与实践 - 01 | Chrome架构:仅仅打开了1个页面,为什么有4个进程? - 单进程浏览器时代open in new window

现代浏览器是多进程的

现代浏览器解决了单进程浏览器的问题,进化为多进程浏览器。

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个 Tab 页,就相当于创建了一个独立的浏览器进程

浏览器包含哪些进程

浏览器主要包含这些进程:

  • 浏览器主进程(Browser Process):浏览器的主进程负责协调、主控,仅有一个。作用有
    • 负责各个页面的管理,创建和销毁其他进程。
    • 负责浏览器界面显示,与用户交互。如前进,后退等。
    • 将渲染进程得到的内存中的 Bitmap,绘制到用户界面上。
    • 提供存储功能。
  • 渲染进程(Renderer Process),该进程内部是多线程的。
    • 核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,包含如下子线程
      • 预解析线程
        • 渲染进程接收到 HTML 内容之后会开启预解析线程,分析 HTML 内容里包含的 JavaScript、CSS 等文件,并提前下载这些文件
        • BTW,若是想验证预解析线程的功能,可以找一个存在多个外链 JS 文件里的页面:
          • 打开页面后,将第一个 JS 文件通过 Charles 等工具阻塞住
          • 查看 Network,会发现在第一个 JS 阻塞时,之后的 JS 文件已经加载完成
      • 页面渲染(GUI 线程)
      • 脚本执行(JS 引擎线程,比如 V8)
      • 事件处理(事件触发线程)
      • 定时计时(定时器线程)
      • 异步请求(异步 HTTP 请求线程)
      • IO 线程: 负责与其他进程 IPC 进行通信,比如接收用户输入事件、网络事件、设备相关等事件
      • 光栅化线程(池)
      • 合成线程
    • 渲染引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中。
    • 默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程,互不影响。
    • 出于安全考虑,渲染进程都是运行在沙箱模式下
  • 网络进程(Network Process)
    • 负责网络资源加载。
    • 以前是作为浏览器主进程的一个模块,后来独立出来成为单独的进程。
  • GPU 进程(GPU Process):最多一个,用于 3D 绘制等。
  • 插件进程(Plugin Process)
    • 负责插件的运行。每种类型的插件对应一个进程,仅当使用该插件时才创建。
    • 因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

需要注意的是,这些进程中,浏览器主进程、网络进程、GPU 进程都是所有 Tab 共用的。

浏览器多进程的优势

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

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

但是,内存等资源消耗也会更大。

浏览器渲染进程,多线程

PS: 在本文中,浏览器渲染进程 = Renderer 进程。

浏览器渲染进程是多线程的,包括的线程主要有:

  • GUI 渲染线程
    • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
    • 当界面需要重绘repaint或由于某种操作引发回流reflow时,该线程就会执行
    • 注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
  • JS 引擎线程
    • 也称为 JS 内核,负责处理 Javascript 脚本程序,例如 V8 引擎
    • JS 引擎线程负责解析 Javascript 脚本,运行代码。
    • JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(渲染进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序
    • 同样注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程
    • 归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当 JS 引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
    • 注意,由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)
  • 定时触发器线程
    • 传说中的setIntervalsetTimeout所在线程
    • 浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行)
    • 注意,W3C 在 HTML 标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
  • 异步 http 请求线程
    • 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

浏览器内核中各个线程的关系

浏览器被划分为浏览器内核和渲染内核两个核心模块,其中浏览器内核是由网络进程、浏览器主进程和 GPU 进程组成的,渲染内核就是渲染进程。

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

JavaScript 是可操纵 DOM 的,如果在修改这些元素属性的同时去渲染界面(即 JS 线程和 GUI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎线程为互斥的关系。当 JS 引擎线程执行时,GUI 线程会被挂起。GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。

JS 阻塞页面渲染

从上述的互斥关系,可以推导出,JS 如果执行时间过长就会阻塞页面渲染。

譬如,假设 JS 引擎正在进行巨量的计算,此时就算 GUI 有更新,也会被保存到队列中,等待 JS 引擎空闲后执行。然后,由于巨量计算,所以 JS 引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免 JS 执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染阻塞的感觉。

Web Worker

HTML5 支持 Web Worker,创建 Web Worker 时,JS 引擎线程会想浏览器内核申请开启一个子线程,且 JS 引擎线程与 Web Worker 线程通过特定的方式通信,比如 postMessage API。

因此,若是有非常耗时的工作,需要单独开启一个 Web Worker 线程,Web Worker 线程不会影响 JS 引擎线程,等到计算出结果后,将结果通信给 JS 引擎线程。

定时器线程

当使用setTimeoutsetInterval时,就需要定时器线程计时,计时完成后,就将事件推入到事件触发线程的任务队列里。

为什么不是 JS 引擎计时呢?因为 JS 引擎是单线程的,若是 JS 引擎线程处于阻塞状态,就会影响计时的准确,因此需要单独开一个定时器线程来计时。

setTimeout 模拟 setInterval

setTimeout模拟setInterval的效果,与直接使用setInterval是有区别的。

每次setTimeout计时到达后就会加入任务队列等待执行,执行结束后会继续添加setTimeout来模拟setInterval,因此,相邻两次setTimeout的时间间隔为单次setTimeout回调函数的执行时间 + setTimeout的定时时间(忽略在任务队列的等待时间)。

setInterval是每次都按精确的定时,隔一段时间向任务队列加入一个事件(回调)。若是 JS 引擎一直阻塞,在一段时间内任务队列里可能存在在多个连续的setInterval回调,等到 JS 引擎空闲时,会将任务队列里的这些回调全部取出并执行,但是需要注意的是,连续的setInterval回调只会执行一次。

若是 JS 引擎执行正常,不出现阻塞的情况,正常使用setInterval,两次setInterval回调的间隔时间也比两次setTimeout回调的间隔时间要短一些,而短的这个时间,就是setTimeout回调执行的时间。

疑问

浏览器里的 V8 引擎是个单独的线程吗?

浏览器里的 V8 引擎不是个单独的线程,而是在渲染进程的主线程里。(待权威文档确认)