浏览器的进程与线程

# 浏览器的进程与线程

关于浏览器中资源调度那些事,快来看看吧~

# 浏览器渲染进程中常驻的线程

  1. js 引擎线程(解释执行 js、用户输入、网络请求)
  2. GUI 线程(绘制用户界面、与 js 主线程是互斥的)
  3. http 网络请求线程(处理用户的 get、post 等请求,等返回结果后将回调函数推入任务队列)
  4. 定时触发器线程(setTimeout、setInterval 等待时间结束后把执行函数推入任务队列中)
  5. 浏览器事件处理线程(将 click、mouse 等交互事件发生后将这些事件放入事件队列中)

大部分浏览器中都是具有这 5 个线程的,这些线程是通过UI主线程进行协调运作的。

js 引擎线程和 GUI 线程是互斥的: js 可以操作 DOM 元素,进而会影响到 GUI 的渲染结果,因此 js 引擎线程与 GUI 渲染线程是互斥的。也就是说当 js 引擎线程处于运行状态时,GUI 渲染线程将处于冻结状态。


# 进程

进程是操作系统资源分配的最小单位,每一个进程都有独立的代码和数据空间,程序之间切换会有较大的开销

# 线程

线程是处理器(CPU)进行任务调度的最小单位,在同一个进程内,线程之间共享内存地址,可以访问相同的变量,共同使用一个堆。线程间的数据共享,提高了资源利用率

# 协程

又称微线程,纤程,英文名 Coroutine

协程是用户态的轻量级线程。通常创建协程时,会从进程的堆中分配一段内存作为协程的栈。他最大的特点就是可中断,调度器由用户自行处理,遇到阻塞操作,立刻放弃掉,并且记录当前栈上的数据,阻塞完后立刻再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑,等达到一定条件后,再恢复原来的栈信息继续执行。像 golang、kotlin 都是原生支持协程的语言,协程在 JS 中也有实现——Generator 使得单线程的 JavaScript 可以使用协程。

协程的优势如下:

  • 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。
  • 节约内存:在 64 位的 Linux 中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。
  • 稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
  • 开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的 IO 操作异步化,例如写文件、耗时 IO 请求等。
  • 简化异步任务,以同步方式写异步代码

# CPU

线程是 CPU 调度的基本单位,那 CPU 能区分线程么?

  • 实际情况是:CPU 并不知道线程、进程之类的概念。

CPU 只知道两件事:

  • 1.从内存取指令
  • 2.执行指令,重复 1

指令怎么取呢?

  • 从 PC 寄存器,PC 寄存器指向内存中指令的地址。

指令怎么形成的呢?

  • 程序或者说我们写的函数编译才会形成指令。

我们该如何让 CPU 执行一个函数呢?

  • 显然我们只需要找到函数被编译后形成的第一条指令就可以了,第一条指令就是函数入口。

说了半天 CPU 和线程有什么关系呢?

这时候操作系统就诞生了,操作系统的出现,提高了 CPU 的利用率,时间片分配算法使得多个进程间可以来回切换使用 CPU,实现伪并行

# 伪并行

伪并行是指单核或多核处理器同时执行多个进程,从而使程序更快。 通过以非常有限的时间间隔在程序之间快速切换 CPU,因此会产生并行感。 缺点是 CPU 时间可能分配给下一个进程,也可能不分配给下一个进程。

# 并发和并行

并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

就像前面提到的操作系统的时间片分时调度。我们聊 QQ 和听歌两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说聊 QQ 和听歌是并发的。

并行(Parallel),当系统有一个以上 CPU 时,当一个 CPU 执行一个进程时,另一个 CPU 可以执行另一个进程,两个进程互不抢占 CPU 资源,可以同时进行,这种方式我们称之为并行(Parallel)。

这里面有一个很重要的点,那就是系统要有多个 CPU 才会出现并行。在有多个 CPU 的情况下,才会出现真正意义上的『同时进行』。

并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。

# 用火车来比喻

  • 线程在进程下行进(单纯的车厢无法运行)

  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)

  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)

  • 同一进程下不同线程间数据很易共享(A 车厢换到 B 车厢很容易)

  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)

  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)

  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)

  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"

  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

# Chrome 打开一个页面有多少进程?分别是哪些

浏览器从关闭到启动,然后新开一个页面至少需要:

1 个浏览器进程,1 个 GPU 进程,1 个网络进程,和 1 个渲染进程,一共 4 个进程

后续如果再打开新的标签页:

浏览器进程,GPU 进程,网络进程是共享的,不会重新启动,

然后默认情况下会为每一个标签页配置一个渲染进程。

但是也有例外,比如从 A 页面里面打开一个新的页面 B 页面,而 A 页面和 B 页面又属于同一站点的话,A 和 B 就共用一个渲染进程,其他情况就为 B 创建一个新的渲染进程。

所以,最新的 Chrome 浏览器包括:1个浏览器主进程1个GPU进程1个网络进程多个渲染进程,和多个插件进程

  • 浏览器进程

    负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等。

    以及负责与其他进程的协调工作,同时提供存储功能。

  • GPU进程

    负责整个浏览器界面的渲染。

    Chrome 刚开始发布的时候是没有 GPU 进程的。

    而使用 GPU 的初衷是为了实现 3D CSS 效果。

    随着对用户界面的要求提高,网页、Chrome 的 UI 界面都用 GPU 来绘制,这使 GPU 成为浏览器普遍的需求,最后 Chrome 在多进程架构上也引入了 GPU 进程。

  • 网络进程

    负责发起和接受网络请求,以前是作为模块运行在浏览器进程一时在面的,后面才独立出来,成为一个单独的进程

  • 插件进程

    主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响

  • 渲染进程

    负责控制显示 tab 标签页内的所有内容,核心任务是将 HTML、CSS、JS 转为用户可以与之交互的网页。

    WebKit 渲染引擎V8 引擎都在渲染进程当中。

    默认情况下 Chrome 会为每个 Tab 标签页创建一个渲染进程。

我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成,所以我们来看一下渲染进程中的线程:

# 渲染进程中的线程

  • GUI渲染线程:负责渲染页面,解析 html 和 CSS、构建 DOM 树、CSSOM 树、渲染树、和绘制页面,重绘重排也是在该线程执行
  • JS引擎线程:一个 tab 页中只有一个 JS 引擎线程(单线程),负责解析和执行 JS。它 GUI 渲染进程互斥,只能一个一个来,如果 JS 执行过长就会导致阻塞掉帧
  • 计时器线程:指 setInterval 和 setTimeout,因为 JS 引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作
  • 异步http请求线程: XMLHttpRequest 连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待 JS 引擎空闲执行
  • 事件触发线程:主要用来控制事件循环,比如 JS 执行遇到计时器,AJAX 异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等 JS 引擎处理

# 浏览器的进程、线程模型

Chrome 为例,有四种进程模型,分别是

  • Process-per-site-instance:默认模式。访问不同站点创建新的进程,在旧页面中打开的新页面,且新页面与旧页面属于同一站点的话会共用一个进程不会创建
  • Process-per-site:同一站点使用同一进程
  • Process-per-tab:每一个标签页都创建新的进程
  • Single Process:单进程模式

线程模型中的线程都是干嘛用的呢?

  • MessagePumpForIO:处理进程间通信的线程,在 Chrome 中,这类线程都叫做 IO 线程
  • MessagePumpForUI:处理 UI 的线程用的
  • MessagePumpDefault:一般的线程用到的

每一个 Chrome 的线程,入口函数都差不多,都是启动一个消息循环,等待并执行任务

# 进程间通信的方式有哪些

  • 管道通信:就是操作系统在内核中开辟一段缓冲区,进程 1 可以将需要交互的数据拷贝到这个缓冲区里,进程 2 就可以读取了
  • 消息队列通信:消息队列就是用户可以添加和读取消息的列表,消息队列里提供了一种从一个进程向另一个进程发送数据块的方法,不过和管道通信一样每个数据块有最大长度限制
  • 共享内存通信:就是映射一段能被其他进程访问的内存,由一个进程创建,但多个进程都可以访问,共享进程最快的是IPC方式
  • 信号量通信:比如信号量初始值是 1,进程 1 来访问一块内存的时候,就把信号量设为 0,然后进程 2 也来访问的时候看到信号量为 0,就知道有其他进程在访问了,就不访问了
  • socket:其他的都是同一台主机之间的进程通信,而在不同主机的进程通信就要用到 socket 的通信方式了,比如发起 http 请求,服务器返回数据

# 多窗口之间怎么通信?

没有办法直接通信,需要有一个类似中介者进行消息的转发和接收,比如

  • localStorage:在一个标签页监听 localStorage 的变化,然后当另一个标签页修改的时候,可以通过监听获取新数据

  • WebSocket:因为 websocket 可以实现实时服务器推送,所以服务器就可以来当这个中介者。标签页通过向服务器发送数据,然后服务器再向其他标签推送转发

  • ShareWorker:会在页面的生命周期内创建一个唯一的线程,并开启多个页面也只会使用同一个线程,标签页共享一个线程

  • postMessage:允许来自不同源的脚本采用异步方式进行通信,跨域通信的一种有效的解决方案

    // 发送方
    window.parent().pastMessage('发送的数据','http://接收的址')
    // 接收方
    window.addEventListener('message',(e)=>{ let data = e.data })
    

# 参考文章