进程&线程&协程

进程、线程、协程三者在隔离、调度与用户态任务组织上的区别说明

#status / growing #type / concept

[!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 内核视角看进程/线程”

创建于 2026/3/13 更新于 2026/5/27