每次打开浏览器开几十个标签页,电脑依然能流畅运行,你有没有想过------这背后到底是"谁"在默默工作?为什么一个网页卡死,其他页面却毫发无伤?今天,我们从进程和线程这对"黄金搭档"说起。
从"单核苦力"到"多核天团"
如果把CPU比作一家餐厅的厨房,那么进程 就是一家独立的餐厅------有自己的厨房、食材仓库、用餐区域,一切资源自给自足。而线程就是这家餐厅里的厨师,多位厨师共享同一间厨房、同一批食材,各自负责不同的菜品。
早期的计算机是"单餐厅单厨师"模式------一次只能接待一桌客人(运行一个程序)。后来有了"多餐厅"模式(多进程),再后来每个餐厅里可以请多位厨师(多线程)。这就是我们今天要聊的核心。
进程:资源分配的"大地主"
进程是操作系统进行资源分配和调度的最小单位。你可以把它理解为程序的一次"活着的"执行实例。
当你双击一个应用图标,操作系统就给它划出一块独立的地盘:
- 独立的内存空间(代码段、数据段、堆、栈)
- 文件描述符(能打开哪些文件)
- 环境变量 、信号处理等
每个进程都活在自己的"平行宇宙"里------它有自己完整的地址空间,从 0x00000000 到 0xFFFFFFFF(32位系统),感觉自己独占了整个内存。实际上这是虚拟内存技术带来的"错觉",但正是这种隔离,让进程之间互不干扰。
进程的"身份证":PCB
操作系统怎么管理成百上千个进程?靠的是进程控制块(PCB) ------一个存着进程ID、状态、程序计数器、寄存器快照、内存管理信息的数据结构。每当CPU要切换进程,就把当前进程的PCB存好,再加载下一个进程的PCB,这就是上下文切换。
进程间通信(IPC):跨餐厅送菜有多难?
因为进程间内存隔离,它们想交流得走"专用通道":
- 管道:像传话游戏,单向传递
- 消息队列:像快递柜,放进去等人取
- 共享内存:最像线程间通信,但需要信号量同步
- Socket:跨网络也行
这些方式比直接喊一嗓子(线程间通信)慢得多,但胜在安全可靠。
线程:CPU调度的"打工仔"
线程是CPU调度和执行的基本单位,是进程内的一个执行流。一个进程可以包含多个线程,它们共享进程的内存空间和资源。
如果说进程是"餐厅",线程就是"厨师团队":
- 共用厨房(堆和全局变量)
- 每人有自己的灶台(栈空间,存局部变量和函数调用)
- 各自做各自的菜(执行不同任务)
为什么多线程能提升性能?
对于I/O密集型任务 (网络请求、文件读写),线程在等待I/O时可以主动让出CPU,其他线程趁机执行------这叫并发,虽然单核CPU同一时刻只能跑一个线程,但通过快速切换让人感觉"同时进行"。
对于多核CPU ,不同线程可以跑在不同核心上------这是真正的并行。Chrome浏览器就是典型:每个标签页一个进程,每个进程里又有网络线程、渲染线程、JS主线程、GPU线程等。
线程的"致命诱惑":共享内存
线程间通信太简单了------直接读写同一个变量就行。但简单背后藏着杀机:
c
// 两个线程同时执行这段代码
int balance = 100;
void withdraw(int amount) {
if (balance >= amount) {
balance -= amount; // 危险!
}
}
如果线程1和线程2同时判断 balance >= 100 都成立,然后各自扣款,最终余额可能变成0而不是-100------这取决于执行顺序。这就是竞态条件。
解决方案是互斥锁(Mutex)------每次只有一个线程能进入临界区。
进程 vs 线程:核心对比
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 创建/切换开销大(需要分配独立内存、页表等) | 创建/切换开销小(共享资源,只需切换栈和寄存器) |
| 内存隔离 | 完全独立,一个崩溃不影响其他进程 | 共享内存,一个线程出错可能导致整个进程崩溃 |
| 通信方式 | IPC复杂(管道、消息队列、共享内存+信号量) | 直接读写共享变量(需同步机制) |
| 调度单位 | 操作系统调度 | CPU调度(实际上线程才是调度单位) |
| 典型场景 | 浏览器标签页、系统服务、需要强隔离的应用 | 后台计算、并发请求处理、UI响应与工作线程分离 |
死锁:当两个线程互相"等对方放手"
这是多线程编程最经典的噩梦。设想:
css
线程1:先拿锁A,再拿锁B
线程2:先拿锁B,再拿锁A
如果执行顺序恰好是:
- 线程1拿到了锁A
- 线程2拿到了锁B
- 线程1等锁B,线程2等锁A
------死锁发生了。两个线程永远阻塞,程序卡死。
如何破解?
- 锁顺序:所有线程按相同顺序获取锁(都先A后B)
- 超时机制:尝试获取锁时设置超时,超时则释放已持有的锁并重试
- 银行家算法:检测安全状态再分配资源(太复杂,实际少用)
- 死锁检测:系统定期检查循环等待,主动杀死其中一个进程
Chrome的多进程架构从根源上避免了JS主线程与渲染线程之间的这类死锁------因为不同进程不共享锁,死锁只会影响单个标签页。
实战案例:浏览器为什么是多进程?
你笔记里提到"浏览器多进程架构,每个标签页、插件、GPU都有自己的进程"。这是Chrome带来的革命性设计:
单进程浏览器的痛点(IE6时代)
- 一个标签页崩溃 → 整个浏览器崩溃
- 内存泄漏 → 浏览器越用越卡
- 恶意页面 → 可读取其他标签页数据
多进程架构的优势
- 隔离:标签页A崩溃,标签页B毫发无伤
- 安全:每个进程有独立地址空间,利用操作系统权限隔离
- 性能:渲染进程可以多个,充分利用多核CPU
但进程也有代价------每个标签页启动时分配独立内存,Chrome以"内存杀手"闻名。所以后来的优化是:相同站点(如所有Google页面)共享一个进程,减少开销。
面试高频题:JS为什么是单线程?
你笔记提到"js主线程是单线程",但浏览器明明是多进程多线程的,怎么JS却单线程?
这是由JavaScript的设计初衷决定的------它最初是用于浏览器DOM操作的语言。如果JS是多线程,两个线程同时修改DOM,一个要删除节点,一个要添加子节点,浏览器就疯了。
所以JS引擎采用事件循环(Event Loop)机制:
- 主线程是单线程,顺序执行任务
- 异步任务(网络请求、定时器)交给Web API线程(浏览器其他线程)
- 完成后的回调进入任务队列
- 主线程空闲时从队列取任务执行
这种模型既避免了多线程的复杂同步问题,又能高效处理I/O密集型任务------虽然JS主线程是单线程,但浏览器整体是多线程的。
总结:用对场景才是王道
| 场景 | 推荐方案 |
|---|---|
| 高隔离、高稳定性 | 多进程(浏览器标签页、系统服务) |
| 高并发、高频数据共享 | 多线程(Web服务器、计算密集型) |
| I/O密集型 | 多线程 + 异步IO(Node.js、Nginx) |
| CPU密集型 | 多进程(充分利用多核,避免GIL) |
进程和线程没有绝对的好坏------进程用空间换安全,线程用共享换效率。理解它们的设计取舍,才能在架构设计时做出正确的选择。
最后送大家一句话:"进程是围墙内的独栋别墅,线程是别墅里的多个工匠------既要让他们高效协作,又要防止他们互相拆墙。" 希望这篇博客能帮你理清这对"黄金搭档"的关系,下次面试时从容应对!