1. 从一次线上事故说起:为什么"背概念"解决不了实际问题
1.1 一个请求在系统里到底经过了谁
我之前带过一个团队,有个后端同学在周会上自信地讲"进程是资源分配的最小单位,线程是CPU调度的最小单位,协程是用户态的轻量级线程",说得一字不差。结果没过两周,生产环境出了一个诡异问题:某个接口偶发性延迟飙到十几秒,CPU 跑不满,内存也不高,请求就是卡住不动。最后查出来的原因很简单——线程池被任务队列填满了,新任务全部排队,而前面的任务都在等待一个外部 RPC 的超时返回。这个同学背了完整的概念,却完全没意识到"进程、线程、协程"这三者不是面试题里三个孤立的名词,而是同一套系统里不同层的执行调度策略,任何一个环节选错,都会直接表现为线上事故。
这也是我写这篇文章的初衷。进程、线程、协程,看起来是三个基础概念,但真正把它们理解透的人,反而是那些在真实项目里被坑过、被迫回头研究底层原理的人。我们不搞教科书式铺开,就围绕一个核心问题展开:一份业务代码,从用户请求进来,到系统返回结果,到底是谁在搬运数据、谁在切换上下文、谁在保护资源边界。把这条链路看清了,你对三者的理解和那些只会背概念的人,绝对是两个层次。
我先说一个大家每天都在经历的场景。你点开一个网页,请求从网卡进来,落入操作系统内核,内核把数据交给监听端口的那个进程;进程内部,某个工作线程从 epoll 事件里捞到这个连接,反序列化出业务参数;如果业务代码用了协程(比如 Kotlin 协程或 C++20 协程),这个线程可能把任务"挂起"到协程调度器里,等下游 Redis 返回后继续执行。整个过程里,进程提供了资源隔离的边界,线程提供了并发执行的载体,协程则负责让一个线程能在等待 I/O 时干更多活。三者是协作关系,不是替代关系。
1.2 概念背了很多,但最常见的三个误解
这些年我在各种技术群里看到过太多对这三个概念的误解,先挑三个最常见的说开。
第一个误解是"协程会取代线程"。这个说法错得离谱。协程自己不会跑,它必须依托线程来执行。你在一台只有 4 核的机器上开 10 万个 Kotlin 协程,底层的线程数可能只有几十个,真正在 CPU 上执行的永远是被调度到的线程。协程的意义在于"让有限线程尽量不被阻塞浪费",而不是"不要线程"。
第二个误解是"多线程一定比多进程快"。单看请求处理,线程确实因为共享地址空间、切换成本更低而有优势,但线程共享也就意味着一个线程越界写内存,整个进程直接崩掉,所有请求全部断。进程隔离虽然重,但它给系统画了一条明确的"崩溃边界"。金融交易系统、浏览器内核、游戏服务器的主逻辑,很多都敢用多进程模型来保命,就是看中这个隔离性。
第三个误解是"并发量越高就越要开多的线程"。无数人栽在这里。线程是对系统资源的直接占用,每个线程都要占一块栈内存,还要参与内核调度。8 核机器上开 256 个工作线程做纯计算,CPU 大量时间会花在上下文切换上,实际吞吐量反而比 16 个线程更低。理解这个,你才能真正明白为什么后面要引入协程来做高并发。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三者真正差别:资源边界、调度权和切换成本
2.1 操作系统视角下的资源分配
先建立一个最基础的坐标系。程序要运行,必须有三样东西:代码和堆栈所在的地址空间、内核分配的文件描述符和内存页面、以及 CPU 的执行权。
进程是这三样东西的"所有者"。每个进程都有独立虚拟地址空间,A 进程指针指向的 0x7fff1234 和 B 进程的 0x7fff1234 完全不是一个物理地址。进程之间不能互相修改对方内存,访问对方文件描述符也需要显式传递(比如通过 fork 继承或 Unix 域套接字传 fd)。这种隔离是操作系统的安全底线,也是进程模型"重"的根源。
线程则是在同一个进程地址空间里"共享"这些资源。一组线程共享代码段、数据段、堆、打开的文件描述符和信号处理方式,唯一独立的是每个线程自己的栈、寄存器内容和线程局部存储(TLS)。因为共享,所以切换时不需要切换地址空间、不需要刷 TLB,这就是线程比进程快的核心原因。也正是因为共享,一个线程对全局变量的修改,其他线程能直接看到——这是并发问题的温床,后面专门说。
协程从操作系统角度根本没有独立"资源"这一说,它连内核都不认识。协程本质上是一个函数执行流程的"可暂停/可恢复状态",编译器或运行时把当前的局部变量、程序计数器、上下文寄存器保存到一块内存里,等恢复时再还原。内核看到的只有线程,协程只是线程内部自己维护的一套调度单元。
2.2 调度的归属:内核态还是用户态
这是三者最根本的分水岭。进程和线程的调度权都在内核手里,由操作系统的调度器决定哪个进程/线程能在哪个 CPU 核心上运行、运行多久。你无法在应用层精确控制一个线程什么时候被换下去,内核根据优先级、时间片、负载情况做全局调度。所以进程和线程切换时,必然涉及用户态到内核态的陷阱切换,这是一笔不小的开销。
协程的调度权完全在用户态,由程序自己实现的调度器(协程库/运行时)决定切换时机。因为没有内核态参与,协程切换时不需要系统调用、不需要陷入内核、不需要执行调度器里的复杂逻辑,只需要保存/恢复少量寄存器状态。这就是协程切换成本能比线程低几个数量级的原因。
但注意,这种"用户态调度"是有前提的。协程库能接管调度,前提是协程自己"主动让出"执行权,比如调用挂起函数等待 I/O。如果一个协程里写了个 while(1); 忙循环,调度器根本没法把它切下去,整个线程会被这个协程卡死。内核线程就没有这个问题,因为内核可以用时钟中断强制剥夺 CPU。这点经常被忽略,但是理解协程模型的关键。
2.3 切换成本不是玄学:从微秒到几十纳秒
我给一个不绝对但足够参考的数量级感受,这些都是实测经验范畴:
- 进程上下文切换:几微秒到十几微秒,具体取决于是否涉及 TLB 刷新、缓存失效、页表切换。如果 A/B 两个进程频繁切换,CPU 的 L1/L2 缓存命中率会大幅下降,实际等效成本比这个数据还要高。
- 线程上下文切换:同进程内线程切换通常比进程切换快,但依然需要陷入内核完成调度,量级在几微秒左右。高并发场景下,大量线程频繁切换依然能让 CPU 空转。
