Appearance
浏览器进程与线程
问题
进程和线程有什么区别?Chrome 的多进程架构是什么?浏览器渲染进程有哪些线程?
结论
进程 vs 线程
| 维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配和拥有的最小单位 | CPU 调度和执行的最小单位 |
| 资源 | 拥有独立内存空间 | 共享所属进程的内存和资源 |
| 隔离 | 崩溃不影响其他进程 | 一个线程崩溃可能影响整个进程 |
| 通信 | IPC(进程间通信),成本较高 | 直接读写共享内存,成本低 |
| 开销 | 创建/切换开销大 | 创建/切换开销小 |
一句话总结:进程是资源容器,线程是执行单元;同一进程内的线程共享内存,不同进程之间相互隔离。
Chrome 多进程架构
Chrome 采用多进程架构,主要包含以下 5 类进程:
| 进程 | 职责 |
|---|---|
| 浏览器进程 | 负责界面显示、用户交互、子进程管理、存储等 |
| GPU 进程 | 处理 GPU 任务(3D 绘制、视频解码等),独立运行防止 GPU 驱动崩溃影响整体 |
| 网络进程 | 负责网络资源加载(HTTP 请求等) |
| 渲染进程 | 负责将 HTML/CSS/JS 渲染成页面;运行在沙箱中,崩溃不影响浏览器主进程 |
| 插件进程 | 每个插件独立运行,插件崩溃不影响浏览器 |
为什么要多进程?:单进程时任意标签页崩溃会导致整个浏览器崩溃;多进程隔离后,一个标签页渲染进程崩溃只影响该标签页,整体稳定性大幅提升。
渲染进程的线程组成
渲染进程是前端最关注的进程,内部包含以下 5 种线程:
① GUI 渲染线程
- 解析 HTML/CSS,构建 DOM 树、CSSOM 树、渲染树
- 负责页面重绘(repaint)和回流(reflow)
- 与 JS 引擎线程互斥:JS 执行时 GUI 线程挂起,GUI 更新进入队列等待
② JS 引擎线程
- 执行 JavaScript 脚本(V8 引擎)
- 同一时刻只有一个 JS 引擎线程在运行(JavaScript 单线程)
- 长时间执行会阻塞 GUI 渲染,导致页面卡顿
③ 事件触发线程
- 控制事件循环(Event Loop)
- 当 JS 引擎执行 setTimeout/ajax/点击事件等时,将回调添加到此线程管理的任务队列
- 等 JS 引擎空闲时,将队首任务推入执行
④ 定时器触发线程
- 专门负责 setTimeout / setInterval 计时
- 计时结束后将回调推入事件队列(不直接执行)
- 注意:W3C 规定定时器最小精度为 4ms,小于 4ms 的一律按 4ms 处理
⑤ 异步 HTTP 请求线程
- XHR 发起请求后,浏览器新开一个线程处理网络请求
- 状态变更时,若有回调函数,将其推入事件队列等待执行
为什么 JS 是单线程?:如果 JS 可以多线程同时操作 DOM,就可能出现两个线程同时修改同一个 DOM 节点的竞态问题,浏览器无法决定以哪个结果为准。通过单线程 + 事件循环的模型,JS 保证了 DOM 操作的安全性。
进程间通信方式(IPC)
进程之间不共享内存,需要通过 IPC 机制传递数据,常见方式有六种:
① 管道(Pipe)
- 内核中的缓冲区,进程 A 写入、进程 B 读取
- 只能单向通信,只能在有血缘关系的进程间使用,生命周期随进程
② 消息队列(Message Queue)
- 消息的链表,支持按消息类型独立接收
- 避免命名管道的阻塞问题,但每条消息有最大长度限制
③ 信号量(Semaphore)
- 本质是计数器,用于进程间的互斥与同步
- 例:信号量为 1,进程 A 访问时置 0,进程 B 见 0 则等待
④ 信号(Signal)
- Unix 系统中最古老的 IPC 方式
- 操作系统通知进程某种预定义事件(如 SIGKILL、SIGTERM)
⑤ 共享内存(Shared Memory)
- 最快的 IPC 方式:多个进程映射同一块物理内存
- 通常需要配合信号量防止竞态条件
⑥ Socket
- 可跨主机通信(网络 IPC)
- 浏览器发起 HTTP 请求就是 Socket 通信的典型例子
僵尸进程和孤儿进程
- 孤儿进程:父进程退出,但子进程仍在运行。孤儿进程会被
init(PID=1)进程收养,由init负责状态收集。 - 僵尸进程:子进程已结束,但父进程没有调用
wait()释放其资源,子进程的进程描述符仍留在系统中。僵尸进程无法被kill,只能等父进程释放或结束。
死锁产生的原因及解决
死锁的四个必要条件(四者同时满足才会死锁):
| 条件 | 说明 |
|---|---|
| 互斥条件 | 资源在某时刻只能被一个进程占用 |
| 请求与保持 | 进程持有资源的同时,还在请求其他资源 |
| 不可剥夺 | 进程已获得的资源只能由自己释放,不可强制剥夺 |
| 环路等待 | 存在进程—资源的环形等待链 |
预防死锁的方法(破坏上述条件之一):
- 资源一次性分配(破坏"请求与保持")
- 请求新资源前先释放已有资源(破坏"不可剥夺")
- 按固定顺序申请资源(破坏"环路等待")
浏览器多标签页通信
标签页之间没有共享内存,需要借助中介者传递消息,常见方案:
- WebSocket:通过服务器中转,服务器向各标签页推送消息
- SharedWorker:多个页面共享同一个 Worker 线程,线程作为中介
- localStorage + storage 事件:一个标签页写入,另一个监听
storage事件 - BroadcastChannel(现代浏览器):同源页面间直接广播消息
- postMessage:持有目标窗口引用时可直接发消息
Service Worker 的理解
Service Worker 是运行在浏览器背景中的独立线程(不能直接访问 DOM),必须使用 HTTPS 协议。
主要用途:离线缓存、请求拦截、后台同步、消息推送。
实现缓存的基本步骤:注册 → 安装时缓存文件 → 拦截 fetch 返回缓存。
javascript
// 注册
if (navigator.serviceWorker) {
navigator.serviceWorker.register('sw.js')
}
// sw.js:安装时缓存
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(cache =>
cache.addAll(['./index.html', './index.js'])
)
)
})
// 拦截请求,有缓存直接返回
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(response => response || fetch(e.request))
)
})