← 返回文章

进程、线程、协程

发布于 2026/10/10

1. 完整回答

进程、线程和协程,主要区别在于资源隔离、执行调度和并发组织的层次不同。

进程可以看成运行中的程序实例,通常拥有独立的虚拟地址空间,用来管理和隔离内存等资源。比如浏览器把不同工作负载放在不同进程里,一个进程出问题,一般不至于直接破坏其他进程的内存。不过进程之间不能直接读写彼此的普通内存,通信通常要通过管道、套接字或共享内存。

线程是进程内的执行单元,也是 Linux 等主流系统实际进行 CPU 调度的对象。同一个进程里的线程共享地址空间和大部分资源,但各自有独立的执行栈、寄存器状态。共享数据很方便,同时也会带来竞态条件,需要用锁、原子操作等方式保证正确性。

协程则是在更上层组织执行的轻量级任务,通常由语言运行时或框架管理。比如一个请求要等待数据库结果,协程可以在等待时挂起,先让其他任务运行,结果回来后再继续执行,这样不用为每个等待中的任务长期占用一个系统线程。Python 的 asyncio 主要通过事件循环和 await 协作调度;Go 的 goroutine 则能由运行时分配到多个系统线程上执行,支持多核并行。

所以实际选型时,需要故障隔离就考虑多进程,需要共享内存并行计算可以用多线程,大量 I/O 等待的并发任务适合协程。它们不是互相替代的关系,真实服务里也经常组合使用。

2. 核心原理与关键概念

2.1 三者的关系

一个进程可以包含多个线程;一个线程可以承载多个协程。 这是常见的组织方式,但协程不一定固定绑定某一个线程,具体取决于运行时。

  • 进程层:提供虚拟地址空间和资源边界,通常以进程间通信交换数据。
  • 线程层:内核把可运行线程调度到 CPU 上;线程共享所属进程的资源。
  • 协程层:运行时记录任务执行到哪里,在适当时机挂起、排队并恢复任务;实际执行仍要依靠系统线程。

2.2 调度与切换流程

线程切换: 线程进入可运行状态 → 内核调度器选择线程 → 保存当前线程的执行上下文 → 恢复下一线程的上下文并运行。若还涉及不同进程,可能需要切换地址空间,并带来额外的缓存、地址转换开销。

协程切换,以事件循环为例: 协程执行 → 遇到尚未完成的 await 操作 → 保存可恢复的执行状态并挂起 → 事件循环运行其他任务 → I/O 完成后把原任务设为可运行 → 从挂起位置继续执行。await 不一定每次都会真正挂起,例如等待对象已经完成时就可能直接继续。

并发 I/O 的关键: 发起非阻塞 I/O → 当前任务等待、让出执行机会 → 事件到达后唤醒任务。这样提升的是等待期间的资源利用率,而不是让单次 I/O 本身必然变快。

2.3 必需概念

概念 含义 关键边界
地址空间 进程访问内存时使用的虚拟地址范围 不同进程通常相互隔离;同进程线程共享
执行上下文 程序计数器、寄存器、栈等恢复执行所需的状态 线程由内核管理;协程的状态由运行时或编译器机制维护
抢占式调度 执行单元不主动让出时,调度器仍可中断它 常见于操作系统线程;部分协程运行时也支持抢占
协作式调度 任务运行到可挂起位置时主动让出执行机会 典型例子是 Python asyncio
I/O 密集型 / CPU 密集型 时间主要花在等待外部操作 / 实际计算 决定是优先提高并发承载量还是利用多核算力

3. 高频面试追问

3.1 CPU 调度的到底是进程还是线程?

在 Linux 上,更准确地说,内核调度的是线程。 一个进程可以只有一个线程,也可以有多个线程;每个可运行线程都可以独立参与调度。所以“进程是资源分配单位、线程是调度单位”是合理的概括,但不要理解成操作系统只调度进程、线程只是内部函数。多核机器上,同一进程的多个线程也可能同时在不同核心运行。

3.2 为什么通常说进程切换比线程切换更重?

主要差别在于是否要切换地址空间。 同一进程内切换线程,通常保留共享的地址空间;切换到其他进程,可能涉及不同页表、地址转换缓存和处理器缓存的影响。不过线程切换本身也有调度和缓存成本,不能说没有开销,也不能断言每一次进程切换都一定慢于线程切换。

继续追问:创建子进程是不是马上复制全部内存?

在 Linux 的常见 fork 实现中不是。它通常采用写时复制:初始共享物理页面,某一方真正写入时再按需复制。因此创建进程的成本不能简单理解成“把整个内存复制一遍”。

3.3 进程之间怎么通信?应该怎么选?

常见方式是管道、消息队列、套接字和共享内存;信号更适合通知事件。 有亲缘关系的本机进程,可以用管道传输数据;独立进程或跨机器通信,通常用套接字;需要消息边界和排队,可以选消息队列;大块数据在本机高效交换时,可以考虑共享内存。

共享内存减少了数据搬运,但也意味着双方要自己处理同步和数据一致性。我的选择通常先看是否跨机器、传输量、延迟要求,以及能否接受额外的同步复杂度。

3.4 同一个进程的线程共享什么?协程是不是就不用加锁?

线程共享进程的地址空间、堆和打开的文件等资源,但各有独立栈和执行状态。协程也不能天然保证线程安全。 多线程同时修改共享变量时,可能产生数据竞争,需要互斥锁、原子操作或消息传递。

单线程事件循环里的协程不会在同一瞬间执行 Python 代码,但在 await 前后仍可能发生任务交错。例如先读取余额、等待 I/O、再写回余额,中间其他协程可能已经修改了余额,这时仍需要协调。Go 的 goroutine 还能在多个线程上并行运行,更要注意共享数据竞争。

3.5 协程为什么能挂起后继续执行?async/await 会创建新线程吗?

关键是保存执行状态,而不是新建线程。 运行时或编译器保存协程暂停时的位置和局部状态,等待条件满足后恢复执行。async/await 本身不代表一定创建线程;例如 Python asyncio 可以在同一个事件循环线程上交替运行多个任务。

继续追问:声明了 async 函数就自动并发了吗?

不一定。以 Python 为例,调用 async def 函数通常只是得到协程对象;需要 await 它,或者通过 create_task 等方式交给事件循环调度。要让多个 I/O 请求重叠执行,也要正确创建并发任务,而不是简单地逐个等待完成。

3.6 协程能不能用多核?Python asyncio 和 Go goroutine 有什么区别?

能否并行取决于运行时如何映射到系统线程,不能笼统说协程只能运行在单核上。 Python 常见的单线程 asyncio 事件循环一次只执行一个任务,适合交替处理 I/O;想做 CPU 并行,通常需要其他线程或进程等执行资源。

Go 的 goroutine 则由运行时把大量任务调度到多个操作系统线程上,在具备多个可用 CPU 核心时可以真正并行。Go 运行时还支持对 goroutine 的抢占,所以“所有协程都只能主动让出”也不准确。

3.7 协程一定比线程快吗?为什么不能直接替代线程?

不一定。协程的主要优势是较低的任务管理成本,以及高并发等待时更高的线程利用率,不是单个计算任务一定执行得更快。 协程最终还是在线程上运行,计算量不会凭空减少。

如果任务一直在做 CPU 计算,单线程事件循环中的其他任务就难以得到执行;如果调用了不能配合调度器的阻塞接口,也可能占住系统线程。另外,协程同样要处理取消、超时、资源上限和共享状态。对于需要多核计算或者兼容阻塞库的场景,多线程或多进程仍然有价值。

3.8 协程里调用阻塞 I/O,会发生什么?

取决于阻塞调用有没有被运行时接管。 在 Python 的单线程 asyncio 中,直接执行同步阻塞函数,比如 time.sleep 或同步网络请求,会阻塞事件循环线程,导致其他协程也无法及时运行。应该优先使用异步客户端;暂时无法替换时,可以把阻塞调用交给线程池。

Go 对常见网络 I/O 和部分系统调用有运行时配合,某个 goroutine 等待时,其他 goroutine 通常还能继续执行。但这不意味着任何阻塞调用都没有成本,尤其要关注外部库、线程占用和资源耗尽。

3.9 I/O 密集型和 CPU 密集型任务应该怎么选?

I/O 密集型优先考虑用异步 I/O、协程或有上限的线程池提高并发;CPU 密集型重点考虑如何利用多核。 比如上万请求大部分时间都在等待数据库,协程能减少大量线程同时等待的开销;但如果每个任务都在压缩图片,增加协程数量并不会自动获得更多 CPU 算力。

在 Go 中,可以让多个 goroutine 借助多个系统线程并行计算;在常见启用 GIL 的 CPython 构建中,纯 Python 的 CPU 密集计算往往更适合多进程,或者使用能够释放 GIL 的计算库。最终还要用吞吐、延迟、CPU 利用率和内存数据验证。

继续追问:Java 虚拟线程和协程是一回事吗?

不是完全等价的概念。Java 虚拟线程是 JVM 管理的轻量级 Thread,可以把大量虚拟线程复用到少量平台线程上;适合大量 I/O 等待任务,但不会让计算本身变快。它和协程解决的问题相近,API 及挂起机制则不同。

3.10 如果设计一个高并发接口服务,三者会怎么组合?

我会按照隔离、执行资源和等待模型分层处理。 首先让服务以独立进程部署,必要时用多个工作进程提高隔离性和资源利用率;进程内保留必要的系统线程,由线程承载请求处理和运行时调度;如果请求主要是数据库、缓存和下游 RPC 等 I/O 等待,就使用协程或异步模型承载大量并发请求。

不会无限创建任务,而是设置连接池容量、并发上限、超时和取消机制,避免下游变慢时把请求全部堆在内存里。如果出现延迟升高,会先区分是 CPU 打满、事件循环被阻塞、锁竞争,还是数据库连接池排队,再决定是增加工作进程、调整线程资源,还是改进异步调用链。

4. 易混淆概念对比

4.1 进程与线程

对比维度 进程 线程
主要职责 提供资源和隔离边界 提供可被内核调度的执行流
内存关系 通常有独立虚拟地址空间 同进程线程共享地址空间,各自有栈
数据交换 通常需要 IPC 可直接访问共享数据,但要同步
故障影响 普通进程间隔离较强 严重内存错误可能影响整个进程

4.2 系统线程与协程

对比维度 系统线程 协程
调度主体 操作系统内核 语言运行时或框架
执行依赖 被内核分配 CPU 仍需在线程上执行
切换方式 内核调度,通常支持抢占 依具体实现,可协作,也可被运行时抢占
主要优势 多核执行、兼容同步阻塞代码 轻量任务管理、高并发等待

4.3 并发与并行

概念 关注点 简单例子
并发 多个任务在同一时间段内交替推进 单个事件循环交替处理多个网络请求
并行 多个任务在同一时刻实际执行 多核 CPU 同时执行多个计算线程

并发不必然并行,并行也不是协程独有的能力。

4.4 协程与异步 I/O

概念 解决的问题 能否独立存在
协程 如何把任务写成可暂停、可恢复的执行流程 可以;不保证调用本身非阻塞
异步 I/O 如何在等待 I/O 完成期间不长期阻塞执行线程 可以;也能通过回调等方式使用

两者经常配合,但写了 async/await 不等于底层调用一定非阻塞。

4.5 Python asyncio、Go goroutine 与 Java 虚拟线程

机制 典型调度方式 特别需要注意
Python asyncio 事件循环内协作调度 同步阻塞调用会卡住事件循环
Go goroutine 运行时把任务复用到多个系统线程,可抢占 可以多核并行,但仍可能发生数据竞争
Java 虚拟线程 JVM 把虚拟线程挂载到平台线程运行 擅长大规模 I/O 等待,不会加快 CPU 计算

5. 总结

进程、线程和协程对应的是三个不同层次:进程负责资源隔离,线程负责实际执行和内核调度,协程负责用更轻量的方式组织并发任务。同进程的线程共享内存,所以通信方便,但要处理数据竞争;不同进程一般隔离更强,通信需要 IPC。协程在等待 I/O 时可以挂起,让线程去运行其他任务,因此特别适合大量请求同时等待的场景。不过协程不等于新线程,也不一定只能单核运行,像 Go 的 goroutine 就能借助多个系统线程并行。实际选择时,我主要看是否需要故障隔离、是否要利用多核计算,以及任务是不是有大量 I/O 等待。