浏览器多线程和js单线程

9,056 阅读11分钟

一、什么是进程和线程

在涉及浏览器多线程和js单线程之前,我们先铺垫一下前置概念:

1、进程(process)

进程和线程都是操作系统的概念。

进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。

当我们启动一个应用,计算机会至少创建一个进程,cpu会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。

mac电脑可以在活动监视器中查看启动的进程数:

活动监视器

2、线程(thread)

  • 进程内部的一个执行单元,是被系统独立调度和分派的基本单位。系统创建好进程后,实际上就启动执行了该进程的主执行线程

  • 进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程。

  • 线程自己不需要系统重新分配资源,它与同属一个进程的其它线程共享当前进程所拥有的全部资源。 PS: 进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。

而现在通用叫法单线程与多线程,都是指在一个进程内的单和多。

如果对进程及线程的理解还存在疑惑,可以参考下述文章👇 www.ruanyifeng.com/blog/2013/0…

关于单核处理器、多核处理器、多处理器是怎么处理进程和线程的,可以参考下述文章👇 blog.csdn.net/alinshen/ar… jsonliangyoujun.iteye.com/blog/235827…

二、浏览器的多进程

其实如果要开发一个浏览器,它可以是单进程多线程的应用,也可以是使用 IPC 通信的多进程应用。

不同浏览器采用了不同的架构模式,这里咱们只研究以Chrome为代表的浏览器:

Chrome 采用多进程架构

Chrome 的不同进程

每打开一个tab页,就相当于于创建了一个独立的浏览器进程,这一点从上面的图中可以看出,但是也不是绝对的,它也有自己的优化机制,有的进程可能会被合并。

1、Chrome 的主要进程及其职责

  • Browser Process 浏览器的主进程(负责协调、主控) (1)负责包括地址栏,书签栏,前进后退按钮等部分的工作 (2)负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问 (3)负责各个页面的管理,创建和销毁其他进程
  • Renderer Process 负责一个 tab 内关于网页呈现的所有事情,页面渲染,脚本执行,事件处理等
  • Plugin Process 负责控制一个网页用到的所有插件,如 flash 每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU Process 负责处理 GPU 相关的任务

不同进程负责的浏览器区域示意图

Chrome 还为我们提供了「任务管理器」,供我们方便的查看当前浏览器中运行的所有进程及每个进程占用的系统资源,双击还可以查看更多类别信息。

通过「页面右上角的三个点点点 — 更多工具 — 任务管理器」即可打开相关面板。

任务管理器

2、Chrome 多进程架构的优缺点

优点: (1)某一渲染进程出问题不会影响其他进程 (2)更为安全,在系统层面上限定了不同进程的权限

缺点: (1)由于不同进程间的内存不共享,不同进程的内存常常需要包含相同的内容。为了节省内存,Chrome 限制了最多的进程数,最大进程数量由设备的内存和 CPU 能力决定,当达到这一限制时,新打开的 Tab 会共用之前同一个站点的渲染进程。

Chrome 把浏览器不同程序的功能看做服务,这些服务可以方便的分割为不同的进程或者合并为一个进程。

以 Broswer Process 为例,如果 Chrome 运行在强大的硬件上,它会分割不同的服务到不同的进程,这样 Chrome 整体的运行会更加稳定,但是如果 Chrome 运行在资源贫瘠的设备上,这些服务又会合并到同一个进程中运行,这样可以节省内存。

三、浏览器的多线程

对我们fe来讲,最重要的是Renderer Process下的多线程,就是我们常说的浏览器内核。

Chrome浏览器为每个tab页面单独启用进程,因此每个tab网页都有由其独立的渲染引擎实例

浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

1、GUI 渲染线程

负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了.

ps:重排和重绘的区别,有兴趣的话,请看👇的链接 juejin.cn/post/684490…

2、JavaScript引擎线程

JS内核,负责处理Javascript脚本程序。 一直等待着任务队列中任务的到来,然后解析Javascript脚本,运行代码。一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序。

ps: GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

3、定时触发器线程

  • 定时器setInterval与setTimeout所在线程
  • 浏览器定时计数器并不是由JavaScript引擎计数的 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

4、事件触发线程

  • 用来控制事件轮询,JS引擎自己忙不过来,需要浏览器另开线程协助
  • 当JS引擎执行代码块如鼠标点击、AJAX异步请求等,会将对应任务添加到事件触发线程中
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理任务队列的队尾,等待JS引擎的处理
  • 由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

5、异步http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。

let xhr = new XMLHttpRequest();   // 不兼容ie6及更低,创建ajax实例
  xhr.open('get',`json/banner.json?_${Math.random()}`); //打开请求:发送请求前的一些配置项,get有缓存,所以要加随机数,post不用
  xhr.onreadystatechange = ()=>{
     // 事件监听,ajax状态改变事件,基于这个事件可以获取服务器返回的响应头主体内容(响应头先回来)
     // 从这步开始,当前ajax任务开始,如果是同步的,后续代码不执行,要等到ajax状态成功后再执行,如果是异步的,不会
      if(xhr.readyState === 4 && /^(2|3)\d{2}$/.test(xhr.status)){
          //readyState 请求状态   // status 返回状态
          let data = JSON.parse(xhr.responseText);  // 获取响应主体的内容
      }
  };
  xhr.send(null); // 发送 ajax (括号中传递的内容就是请求主体的内容)

这是一个简单的XMLHttpRequest请求的四个步骤,基于XMLHttpRequest的代表产品$.ajax,axios等

三、Js为什么要是单线程?

js的单线程和它的用途有关,作为浏览器脚本语言,它主要是用来处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。

如果JavaScript是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;

如果Javascript是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。

当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行

这也解释了为什么GUI线程和JS引擎是互斥的。

当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

为了多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

四、Js的单线程会带来什么问题?怎么处理的?

当调用栈中的函数调用需要花费我们非常多的时间,会发生什么?

比如正在运行一个复杂的图像转换的算法

当调用栈有函数在执行,浏览器就不能做任何事了 —— 它被阻塞了。这意味着浏览器不能渲染页面,不能运行任何其它的代码,它就这样被卡住了。那么问题来了 —— 你的应用不再高效和令人满意了。

一旦你的浏览器开始在调用栈运行很多很多的任务,它就很有可能会长时间得不到响应。在这一点上,大多数的浏览器会采取抛出错误的解决方案,询问你是否要终止这个页面:

但这会毁了你的用户体验

js开发的时候有一个很重要概念:

之后 的代码并不一定会在 现在 的代码执行之后执行。换句话说,在定义中不能 现在 立刻完成的任务将会异步执行,这意味着可能不会像你认为的那样发生上面所说的阻塞问题。

谁会告诉 JS 引擎去执行你的程序?事实上,JS 引擎不是单独运行的 —— 它运行在一个宿主环境中,对于大多数开发者来说就是典型的浏览器和 Node.js。实际上,如今,JavaScript 被应用到了从机器人到灯泡的各种设备上。每个设备都代表了一种不同类型的 JS 引擎的宿主环境。

所有的环境都有一个共同点,就是都拥有一个事件循环内置机制,它随着时间的推移每次都去调用 JS 引擎去处理程序中多个块的执行。

这意味着 JS 引擎只是任意的 JS 代码按需执行的环境。是它周围的环境来调度这些事件(JS 代码执行)。,其他周围环境就是指的在同一进程下的不同的线程,比如事件触发线程、定时器线程等

所以,比如当你的 JavaScript 程序发出了一个 Ajax 请求去服务器获取数据,你在一个函数(回调)中写了 “response” 代码,然后 JS 引擎就会告诉宿主环境: “嘿,我现在要暂停执行了,但是当你完成了这个网络请求,并且获取到数据的时候,请回来调用这个函数。”

然后浏览器设置对网络响应的监听,当它有东西返回给你的时候,它将会把回调函数插入到事件循环队列里然后执行。

浏览器种的EventLoop

如果你想详细的了解这方面的内容,👇的链接: web.jobbole.com/95613/