进程&线程&协程
进程、线程、协程三者在隔离、调度与用户态任务组织上的区别说明
[!info] related notes 操作系统 MOC 进程 / 线程 / 协程(面试速记) javascript中的进程线程协程
进程&线程&协程
可以把这三个概念先压成一句话:
进程是资源分配和隔离的基本单位,
线程是操作系统调度 CPU 的基本执行单位,
协程是用户态自己管理切换时机的轻量执行单元。
最容易混淆的是:它们不是同一层面的东西。
进程、线程主要是操作系统层面的抽象,协程更多是语言运行时或应用框架层面的抽象。
1. 先建立一个整体图景
把程序运行想成一家工厂:
- 进程像一个独立车间
有自己的地址空间、打开的文件、内存映射、权限边界 - 线程像车间里的工人
多个工人共享车间资源,但各自有自己的工作位置和执行现场 - 协程像工人脑子里的多个“任务清单”
一个工人可以主动暂停手头任务,切到另一个任务,再切回来
所以:
- 进程解决的是 隔离与资源拥有
- 线程解决的是 并发执行与 CPU 调度
- 协程解决的是 在少量线程上高效组织大量任务
2. 什么是进程
2.1 进程的本质
进程可以理解成:
一个正在运行的程序实例,以及它运行所需要的那整套资源和上下文。
一个进程通常包含:
- 独立的虚拟地址空间
- 代码段、数据段、堆
- 至少一个线程
- 打开的文件描述符/句柄
- 信号处理状态
- 环境变量
- 当前工作目录
- 用户权限信息
- 各种内核维护的控制信息
也就是说,程序是静态文件,进程是程序跑起来后的动态实体。
2.2 为什么需要进程
进程最核心的价值是隔离。
比如浏览器、编辑器、数据库同时运行时:
- 它们通常互相看不到对方的内存
- 一个进程崩了,不应该直接把另一个进程的地址空间搞坏
- 操作系统可以按进程做权限控制、资源记账、回收资源
所以,进程是操作系统做安全隔离、故障隔离、资源管理的重要边界。
2.3 进程的地址空间
这是理解进程最关键的地方。
每个进程通常认为自己“拥有一整块连续内存”,但这其实是虚拟地址空间。
不同进程里同样的虚拟地址,往往映射到不同的物理内存。
典型布局大概是:
- 代码段:程序指令
- 数据段:全局变量、静态变量
- 堆:动态分配内存
- 栈:线程调用栈
- 共享库映射区
- 内核映射区
所以两个进程即使都访问 0x12345678,看到的也通常不是同一块东西。
2.4 进程的创建和销毁
常见过程大致是:
- 创建进程
- 装载程序映像
- 分配地址空间和内核对象
- 创建初始线程
- 开始执行
在类 Unix 系统里,经常是:
fork:复制父进程执行上下文exec:在当前进程地址空间里装入新程序
在 Windows 里更常见的是直接创建新进程并装载程序。
进程结束时,操作系统会回收它持有的大部分资源。
2.5 进程间通信为什么麻烦
因为进程彼此隔离,默认不能直接共享内存。
所以进程之间要通信,通常需要 进程间通信方案IPC
这就是为什么“多进程安全但通信成本更高”。
3. 什么是线程
3.1 线程的本质
线程可以理解成:
进程内部的一条执行流。
一个进程至少有一个线程,叫主线程。
也可以有多个线程并发执行同一个进程里的代码。
线程通常有自己独立的:
- 程序计数器 PC
- 寄存器现场
- 栈
- 调度状态
但它和同进程的其他线程共享:
- 地址空间
- 代码段
- 堆
- 全局变量
- 打开的文件
- 大部分进程资源
3.2 为什么需要线程
因为很多场景下,一个进程里需要同时做多件事:
- GUI 程序一边响应界面,一边后台加载数据
- Web 服务器同时处理多个请求
- 数据库同时处理多个客户端连接
- 程序把不同任务分给多个 CPU 核并行处理
线程的好处是:
- 同一进程内共享数据方便
- 线程切换通常比进程切换轻
- 能更好利用多核 CPU
3.3 线程和并行
线程常常和并发、并行一起出现,但要分清:
- 并发:逻辑上同时处理多件事
- 并行:物理上同一时刻真的同时执行
单核 CPU 上,多线程通常是并发,靠时间片轮转。
多核 CPU 上,多线程可以真正并行。
3.4 为什么线程难写
因为线程共享进程内的大量数据,所以会遇到经典并发问题:
- 竞态条件 race condition
- 可见性问题
- 原子性问题
- 死锁
- 活锁
- 饥饿
例如两个线程同时执行:
count = count + 1
这不是原子操作,可能出现丢失更新。
所以线程编程通常需要同步机制:
- 互斥锁 mutex
- 读写锁
- 自旋锁
- 条件变量
- 信号量
- 原子变量
- 屏障 barrier
3.5 线程切换发生了什么
线程切换时,操作系统大致要做:
- 保存当前线程寄存器
- 保存程序计数器、栈指针等上下文
- 更新调度队列
- 切换到另一个线程的内核调度上下文
- 恢复目标线程执行现场
如果切换的是同一进程内线程,地址空间通常不用换。
如果切换的是不同进程的线程,往往还涉及地址空间切换,代价更大。
这也是“线程切换通常比进程切换轻”的原因之一。
4. 什么是协程
4.1 协程的本质
协程可以理解成:
由用户态程序自己调度的、可暂停可恢复的执行单元。
它不是传统意义上由操作系统直接调度的“执行实体”,而更像:
- 一个函数执行到一半可以暂停
- 以后再从暂停点继续执行
- 切换通常发生在用户态,不需要每次都陷入内核
所以协程的核心关键词是:
- 挂起
- 恢复
- 主动让出
- 用户态调度
4.2 为什么需要协程
协程主要解决这个问题:
大量任务都需要等待 I/O,如果每个任务都开一个线程,会太重。
例如高并发网络服务:
- 10 万个连接
- 绝大多数时间都在等网络数据
- 真正执行 CPU 的时间很少
如果一个连接对应一个线程:
- 内存占用很大
- 线程调度开销很大
- 锁竞争严重
而协程可以做到:
- 一个线程内挂很多任务
- 遇到 I/O 等待就主动挂起
- 线程去跑别的协程
- I/O 完成后再恢复原协程
因此协程特别适合 高并发 I/O 密集型 场景。
4.3 协程为什么“轻”
因为协程切换通常不需要:
- 内核态/用户态频繁切换
- 操作系统调度介入
- 切换整个线程上下文那样重的现场
它通常只需要保存少量用户态执行状态:
- 当前执行点
- 局部状态
- 协程栈或状态机位置
所以协程切换往往比线程切换更轻。
4.4 协程不是“自动并行”
这是一个特别常见的误解。
协程通常擅长并发,不天然擅长并行。
如果很多协程跑在一个线程里,那么任意时刻通常只有一个协程真在执行。
它们只是因为切换很快、等待 I/O 时能让出,所以看起来能同时处理很多事。
如果要真正利用多核并行,通常还是需要:
- 多线程 + 协程
- 多进程 + 协程
- 或多个 event loop / worker
4.5 协程的两种常见实现思路
第一种:栈式协程
像“函数被挂起后原地恢复”。
特点:
- 写法接近同步代码
- 可读性强
- 需要保存自己的调用栈
第二种:无栈协程 / 状态机式
像 async/await 编译后变成状态机。
特点:
- 更节省资源
- 更容易和语言运行时结合
- 本质上常是“在若干挂起点之间跳转的状态机”
很多现代语言里的 async/await,本质上都可以理解成某种协程模型。
5. 三者最核心的区别
先看最关键的一张对比表。
5.1 调度者不同
- 进程/线程:主要由操作系统调度
- 协程:主要由用户态运行时/应用程序调度
这是最本质区别。
5.2 资源拥有关系不同
- 进程拥有资源
- 线程依附于进程,共享进程资源
- 协程依附于线程,在线程内运行
常见关系是:
进程 > 线程 > 协程
一个进程里可以有多个线程;
一个线程里可以跑很多协程。
5.3 隔离性不同
- 进程隔离最强
- 线程隔离弱,共享内存
- 协程隔离更弱,通常完全运行在同一线程、同一地址空间里
隔离越强,安全性和稳定性通常越好;
隔离越弱,通信和切换通常越便宜。
5.4 切换成本不同
一般来说大致是:
进程切换 > 线程切换 > 协程切换
因为:
- 进程切换常涉及地址空间切换
- 线程切换需要内核调度和现场切换
- 协程切换多半只是在用户态切换执行上下文
5.5 通信成本不同
- 进程间通信最复杂
- 线程间通信最直接,但同步复杂
- 协程间通信在单线程模型里常最简单,因为很多共享数据无需传统锁
但注意:协程简单往往建立在“同线程串行执行”基础上,不等于没有并发问题。
6. 详细讲讲它们各自的优缺点
6.1 进程的优缺点
优点
- 隔离强,安全性好
- 一个进程崩了不容易直接污染别的进程
- 权限和资源控制清晰
- 适合服务拆分、沙箱、容器化
缺点
- 创建和销毁成本较高
- 上下文切换较重
- IPC 相对复杂
- 数据共享不如线程方便
6.2 线程的优缺点
优点
- 同进程共享数据方便
- 调度单位轻于进程
- 适合多核并行
- 适合 CPU 密集型任务拆分
缺点
- 共享内存导致并发 bug 多
- 锁难写、难调试
- 死锁/竞态隐蔽
- 线程太多时调度成本高、内存占用大
6.3 协程的优缺点
优点
- 切换轻
- 可以挂很多任务
- 非阻塞 I/O 场景效率高
- 代码风格可接近同步逻辑
- 减少线程数量和锁竞争
缺点
- 不天然利用多核
- 一个协程里若执行长时间阻塞操作,可能拖住整个线程
- 调试、调用栈观察、错误传播有时更复杂
- 依赖语言运行时和生态支持
7. 从“阻塞”角度看三者
这个角度特别有助于理解协程。
7.1 进程阻塞
一个进程里如果只有一个线程,这个线程阻塞了,整个进程的执行流就停在那里。
7.2 线程阻塞
一个线程阻塞,别的线程仍然可以运行。
所以多线程常用来把阻塞隔离开。
7.3 协程阻塞
这里要特别小心:
- 如果协程执行的是可挂起的非阻塞操作,那它只是让出执行权
- 如果协程里直接调用了真正的阻塞系统调用,那往往会把它所在的线程一起卡住
所以协程高效的前提通常是:
- 非阻塞 I/O
- 事件循环
- await/挂起点
- 调度器配合
这也是为什么协程经常和 epoll/kqueue/io_uring/event loop 一起出现。
8. 典型应用场景怎么选
8.1 什么场景适合多进程
- 强隔离
- 多服务拆分
- 不同权限边界
- 崩溃隔离要求高
- 利用多核但又不想共享内存太多
比如:
- 浏览器多进程架构
- Web 服务多 worker 进程
- 数据处理管道不同阶段分进程
8.2 什么场景适合多线程
- 需要利用多核并行
- 共享内存数据结构多
- CPU 密集型任务拆分
- 图形界面 + 后台任务
- 线程池处理任务
比如:
- 图像处理
- 科学计算
- 编译器并行任务
- 服务端线程池
8.3 什么场景适合协程
- 高并发 I/O
- 网络服务器
- 爬虫
- 网关
- 大量等待数据库/网络/磁盘返回的任务
比如:
- async Web 服务器
- 聊天连接服务
- RPC 框架
- 代理服务器
9. 它们之间怎么配合
现实系统里,通常不是三选一,而是组合使用。
9.1 多进程 + 多线程
例如服务器开多个 worker 进程,每个进程里再开线程池。
好处:
- 进程级隔离
- 线程级并行
9.2 多线程 + 协程
这是现在很常见的一种高性能模式。
例如:
- 每个 CPU 核一个线程
- 每个线程一个事件循环
- 每个线程里跑大量协程
这样既能利用多核,又能让单线程内大量 I/O 任务轻量并发。
9.3 多进程 + 协程
例如:
- 开多个进程利用多核和隔离
- 每个进程里一个事件循环
- 进程内跑大量协程连接
很多高性能网络服务就是这种结构。
10. 一些经典误区
误区 1:线程就是轻量进程
这是历史上常见说法,但容易误导。
更准确地说:
线程是进程内的执行流,共享进程资源。
它和进程并不是简单“重量级/轻量级”的量变关系,而是角色不同。
误区 2:协程比线程“更高级”,应该全面替代线程
不是。
协程和线程解决的问题不同。
- 协程更擅长 I/O 并发组织
- 线程更适合利用多核并行
- 线程也是协程运行的重要承载体
很多协程程序底层仍然离不开线程池、I/O 线程、调度线程。
误区 3:协程没有并发问题
不对。
如果多个协程共享状态,依然可能有逻辑竞态。
只是如果它们都在同一个线程内、只在显式挂起点切换,问题会比抢占式线程更容易推理。
误区 4:单线程协程一定比多线程快
不一定。
如果任务是 CPU 密集型,单线程协程往往根本跑不过多线程并行。
协程的优势主要在 高并发 I/O 等待,不是所有场景通吃。
11. 用一个例子把三者串起来
假设你在做一个聊天服务器,要支持几十万连接。
方案 A:每连接一个进程
几乎不可行,开销太大,IPC 太重。
方案 B:每连接一个线程
理论上能做,但线程数量太多时:
- 栈内存很大
- 调度成本很高
- 上下文切换重
方案 C:少量进程 / 少量线程 + 大量协程
更现实:
- 开多个进程利用多核和隔离
- 每个进程开少量线程
- 每个线程跑事件循环
- 每个连接对应一个协程
这样绝大多数协程都在等 I/O,不会占着系统线程不放。
反过来,如果你做的是视频编码、矩阵乘法、渲染、搜索排序,瓶颈往往是 CPU,而不是 I/O,这时多线程并行通常比单线程协程更关键。
12. 一句话总结三者
可以这样记:
- 进程:给程序一个独立运行世界,重点是资源与隔离
- 线程:在进程里执行任务,重点是调度与并行
- 协程:在用户态组织大量可挂起任务,重点是轻量并发与高效 I/O
再压缩一点:
进程管“边界”,线程管“执行”,协程管“让出与恢复”。
如果你愿意,我下一条可以继续讲
“线程和协程的调度到底有什么本质区别”,或者直接讲
“从 Linux 内核视角看进程/线程”。