Skip to content

浏览器的进程与线程 ​

浏览器是一个 多进程应用,其中包含多个 线程 来执行不同的任务,如 渲染页面、执行 JavaScript、加载资源、管理网络请求 等。理解浏览器的进程与线程,可以帮助我们优化 性能、避免阻塞、提高用户体验。


1. 进程与线程的概念 ​

  • 进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
  • 线程是进程中的更小单位,描述了执行一段指令所需的时间。

进程是资源分配的最小单位,线程是CPU调度的最小单位。

一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。

如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。

进程和线程之间的关系有以下四个特点:

(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。

(2)线程之间共享进程中的数据。

(3)当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。

(4)进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。

Chrome浏览器的架构图: 从图中可以看出,最新的 Chrome 浏览器包括:

  • 1 个浏览器主进程
  • 1 个 GPU 进程
  • 1 个网络进程
  • 多个渲染进程
  • 多个插件进程

这些进程的功能:

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。

虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:

  • 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
对比项进程(Process)线程(Thread)
定义运行中的程序实例进程内部的执行单元
独立性相互独立(不同进程之间互不影响)共享进程资源(同一进程下的线程共享内存)
通信通过 IPC(进程间通信)通过 共享内存
崩溃影响进程崩溃不会影响其他进程主线程崩溃会影响整个进程
切换开销较大(进程切换慢)较小(线程切换快)

📌 总结

  • 进程是程序运行的独立实体,多个进程可以独立运行。
  • 线程是进程内部的最小执行单位,多个线程共享进程的资源。

2. 浏览器的多进程架构 ​

现代浏览器(如 Chrome、Edge)采用 多进程架构,每个主要功能模块运行在独立进程中。

✅ 常见的浏览器进程 ​

进程作用
浏览器进程(主进程)负责管理窗口、标签页、地址栏、书签等
渲染进程(页面进程)负责HTML 解析、CSS 解析、JavaScript 执行、布局、绘制(核心进程)
GPU 进程负责 页面渲染加速(WebGL、3D 渲染等)
网络进程负责 管理网络请求(HTTP、WebSocket、DNS 解析)
插件进程负责 运行 Flash、PDF 插件

📌 多进程的优点

  • 一个标签页崩溃不会影响整个浏览器(隔离性好)
  • 更好的安全性(防止恶意代码影响整个浏览器)
  • 利用多核 CPU 提高性能

📌 缺点

  • 占用更多内存(每个标签页一个进程,导致 RAM 占用高)

3. 渲染进程的多线程 ​

渲染进程(Renderer Process)是浏览器的核心进程,内部包含多个线程:

线程作用
GUI 渲染线程负责 HTML 解析、CSS 解析、页面布局、绘制
JavaScript 线程(主线程)负责 解析 & 执行 JavaScript
事件触发线程负责 管理异步事件(定时器、DOM 事件)
定时器线程负责 setTimeout、setInterval 计时
HTTP 请求线程负责 异步请求(Ajax、Fetch)

📌 重要特性

  • JavaScript 线程与 GUI 线程是互斥的(防止 UI 渲染冲突)
  • JavaScript 是单线程,执行 JS 时会阻塞 UI 渲染
  • 异步任务(如 Ajax、定时器)由事件触发线程管理

4. 线程之间的工作流程 ​

浏览器的 渲染过程 大致如下:

sh
用户输入 URL → 浏览器进程 → 网络进程发起 HTTP 请求 → 渲染进程解析 HTML/CSS/JS → GUI 线程绘制页面

📌 详细流程

  1. 浏览器进程 接收用户输入 URL,交给 网络进程 请求资源。
  2. 网络进程 发送 HTTP 请求,获取 HTML、CSS、JS、图片等资源。
  3. 渲染进程 解析 HTML,构建 DOM 树。
  4. CSS 解析 生成 CSSOM。
  5. JavaScript 线程 执行 JS(如果 JS 阻塞,GUI 渲染暂停)。
  6. 构建 Render Tree,计算布局(Layout)。
  7. GUI 渲染线程 绘制页面(Paint)。
  8. GPU 进程 负责最终的页面显示。

📌 优化点

  • 减少 JavaScript 阻塞(JS 线程会影响 UI 渲染)
  • 使用 requestAnimationFrame() 优化动画
  • 合理使用 Web Worker,避免主线程阻塞

5. 为什么 JavaScript 线程会阻塞渲染? ​

  • JavaScript 是单线程的
  • GUI 渲染线程与 JS 线程是互斥的
  • 如果 JS 执行时间过长,UI 无法更新

📌 示例

js
// 模拟 JS 阻塞渲染
while (true) {
  console.log("死循环");
}

📌 解决方案

  • 使用 Web Worker 让 JS 运行在后台
  • 使用 requestAnimationFrame() 让动画更流畅
  • 避免长时间同步任务(如复杂计算)

6. Web Worker(多线程 JavaScript) ​

Web Worker 允许在后台线程运行 JavaScript,避免阻塞 UI 渲染。

js
// worker.js
onmessage = function (event) {
  postMessage(event.data * 2); // 计算结果并返回
};
js
// 主线程
const worker = new Worker("worker.js");
worker.postMessage(5); // 发送任务
worker.onmessage = function (event) {
  console.log("计算结果:", event.data);
};

📌 适用于

  • 大规模计算(如图片处理、AI 计算)
  • 避免主线程阻塞,提高页面流畅度

7. 进程之间的通信方式 ​

(1)管道通信 ​

管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。

管道的特点:

  • 只能单向通信
  • 只能血缘关系的进程进行通信
  • 依赖于文件系统
  • 生命周期随进程
  • 面向字节流的服务
  • 管道内部提供了同步机制

(2)消息队列通信 ​

消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。

(3)信号量通信 ​

共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。

(4)信号通信 ​

信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。

(5)共享内存通信 ​

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

(6)套接字通信 ​

上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。

8. 僵尸进程和孤儿进程是什么? ​

  • 孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  • 僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。

9. 死锁产生的原因? 如果解决死锁的问题? ​

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

系统中的资源可以分为两类:

  • 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
  • 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

产生死锁的原因:

(1)竞争资源 ​

  • 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
  • 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

(2)进程间推进顺序非法 ​

若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁

产生死锁的必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程——资源的环形链。

预防死锁的方法:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

7. 如何实现浏览器内多个标签页之间的通信? ​

实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:

  • 使用 websocket 协议,因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。
  • 使用 ShareWorker 的方式,shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。
  • 使用 localStorage 的方式,我们可以在一个标签页对 localStorage 的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。
  • 使用 postMessage 方法,如果我们能够获得对应标签页的引用,就可以使用postMessage 方法,进行通信。

10. 进程 & 线程总结 ​

类别浏览器进程渲染进程(Renderer)
作用管理窗口、标签页解析 HTML/CSS/JS,渲染页面
是否多线程是是
线程-JS 线程、GUI 线程、HTTP 线程
是否隔离是否(线程共享内存)
崩溃影响影响整个浏览器仅影响当前标签页

📌 最佳实践

  • 减少 JavaScript 阻塞
  • 使用 Web Worker 处理复杂计算
  • 减少 reflow(避免频繁修改 DOM)
  • 使用 requestAnimationFrame() 让动画更流畅

🚀 理解浏览器的进程和线程,可以帮助我们优化前端性能,提升用户体验! 🚀

评论区

欢迎留言、补充或勘误。

xiaoba.blog