很多人第一次接触 Kotlin 协程,都会先记住“轻量级线程”这句话,但这句话只能帮你建立直觉,远远不够拿来做技术判断。本文从协程的运行方式、它要解决的异步编程问题,以及“轻量”究竟轻在哪里三条线展开,再补充常见分类,帮助你在看到 suspend、await、调度器这些概念时,能把它们放回正确的语境里理解。
协程到底是什么
提到协程,常见说法是:它是一种可以被挂起和恢复的用户态执行单元。也就是说,函数执行到某个位置时,可以先主动交出控制权,等条件满足后,再从断点继续往下执行。
从这个定义里,可以先抓住三个关键点:
- 用户态控制:协程的调度主要由程序自身或运行时库负责,而不是完全交给操作系统内核,因此切换成本通常比线程更低。
- 协作式调度:协程不会像线程那样随时被系统强行打断,而是在显式挂起时,例如
yield、await,主动交出执行权。 - 状态保存:协程挂起时会保留当前上下文,比如局部变量、执行位置等,恢复后不需要从头开始。
因此,把协程理解成“线程的缩小版”并不准确。更贴切的说法是:协程是运行在线程之上的、更高层的并发抽象。线程仍然是系统调度的基本单位,而协程是在一个或多个线程之内,由运行时协调执行顺序。
这也是为什么一个线程里可以承载多个协程,而协程之间的切换,很多时候并不需要触发一次真正的线程切换。
为什么需要协程
协程之所以重要,本质上是因为它同时回应了两个老问题:异步代码难写、并发任务太重。
异步编程为什么容易失控
在网络请求、文件读写、数据库操作这类 IO 密集型场景里,如果程序傻等结果返回,CPU 往往是在空转。传统做法通常是回调函数,但回调一层套一层,很容易变成难以维护的“回调地狱”。

协程的价值就在这里:它允许你用接近同步代码的写法去描述异步流程。代码看起来像顺序执行,实际却可以在等待 IO 时挂起,而不是阻塞线程。
例如下面这段对比,核心意思并不是具体语言语法,而是两种思路的差异:
httpRequest.get("url", function(response) {
response.on("data", function(data) {
// 处理数据,如果再嵌套其他异步操作...
});
});
// 使用 Async/Await(假设语法上允许这么写,只是做个示例)
async function fetchData() {
let response = await httpRequest.get("url"); // 挂起,等待结果,但不阻塞线程
let data = await response.json(); // 再次挂起等待
return process(data);
}
前一种写法把控制流拆散了,阅读时要不断追踪“回调何时触发、下一步在哪”。后一种虽然底层依旧是异步机制,但业务流程被重新拉直了,代码的可读性和可维护性会明显更好。
并发任务为什么不能一味靠线程
另一个问题是资源成本。线程当然能并发执行任务,但线程不是免费的。它的创建、销毁、上下文切换,以及栈内存占用,都要付出真实的系统代价。当并发量很高时,这些成本会迅速放大。

这也是协程被称为“轻量级”的原因之一:它让大量任务不必都对应成真实线程,而是尽量复用少量线程,把等待中的任务表示为可挂起、可恢复的执行单元。
协程“轻”在哪里
很多介绍会用一个很好理解的比喻:
- 进程像一家公司,拥有独立场地、资金和资源。
- 线程像正式员工,招聘、离职、岗位切换都需要完整流程,管理成本不低。
- 协程更像项目里的临时工作小组,在公司内部快速组建、切换和解散,不需要每次都惊动整套行政体系。
如果把这个比喻翻译回技术层面,协程的“轻”主要体现在下面四个方面。
1. 创建和销毁成本更低
| 维度 | 线程 | 协程 |
|---|---|---|
| 操作者 | 操作系统内核 | 用户程序/运行时库 |
| 系统调用 | 需要(如 clone, pthread_create) | 不需要 |
| 内存分配 | 需要为栈、线程控制块等在内核/用户空间分配内存(通常为 MB 级别,如Linux默认8MB) | 通常在堆上预分配或动态分配一小块内存(通常为 KB 级别,如2KB) |
| 速度 | 慢(毫秒级,ms) | 极快(纳秒级,ns,通常比线程快100-1000倍) |
这里最关键的区别在于,线程创建要经过内核参与,还要准备线程栈和控制块;而协程往往只是运行时创建的一个对象或一段状态。因此,创建数万甚至数十万个协程在很多场景中是现实可行的,但同数量级的线程通常会直接把内存和调度器拖垮。
2. 上下文切换开销更低
性能差异最明显的地方,往往不是“创建一次”,而是“频繁切换”。
| 维度 | 线程切换 | 协程切换 |
|---|---|---|
| 切换者 | 操作系统内核(调度器) | 用户程序/运行时库(协程调度器) |
| 模式切换 | 需要从用户态切换到内核态,再切回用户态。这个操作本身就有CPU开销。 | 完全在用户态完成,没有模式切换的开销。 |
| 保存的上下文 | 非常多。包括:通用寄存器、浮点寄存器、状态寄存器、栈指针、程序计数器、内存映射信息等。需要保存整个CPU现场。 | 非常少。通常只保存必要的寄存器(如程序计数器、栈指针)和少量局部变量。 |
| 缓存影响 | 可能很大。线程被切换到不同CPU核心时,CPU缓存(L1/L2/L3)可能失效,导致性能下降。 | 很小。协程通常在同一个线程内切换,对CPU缓存友好。 |
线程切换需要内核调度,保存和恢复的信息很多,还可能带来缓存失效;协程切换则通常发生在同一线程内部,由运行时在合适的挂起点完成,成本自然低得多。
3. 栈内存更灵活
- 线程一般拥有固定大小的栈空间,例如常见的 MB 级栈。即便实际只用了很小一部分,这块内存往往也要先保留下来。
- 协程通常不会为每个任务都准备一整块固定大栈,而是采用动态增长或在堆上保存状态,只保留当前真正需要的那部分信息。
这意味着,当系统里同时存在大量等待中的任务时,协程模型在内存利用率上通常更有优势。
4. 调度方式更可控
- 线程是抢占式调度:操作系统可能在几乎任何时刻暂停当前线程,切换到另一个线程。
- 协程是协作式调度:只有当代码执行到
yield、await这类明确挂起点时,才会交出执行权。
这带来两个直接结果:
- 切换时机更明确,控制流更容易推理。
- 在单线程协程模型里,非挂起点的代码段通常具有较强的连续性,不必像多线程抢占那样处处担心被突然打断。
这并不等于协程场景就完全没有并发问题,但它确实让很多异步代码的理解成本和同步成本下降了。
一个更准确的结论
所以,“协程是轻量级线程”适合作为入门口号,但不适合作为严格定义。更准确地说:
协程是运行在线程之上的、由用户程序控制的并发抽象;它之所以轻,是因为把大量调度与状态管理工作放在用户态完成,尽量避免频繁陷入内核。
协程有哪些常见分类
对日常应用开发来说,分类不是最先要掌握的部分,但它能帮你理解不同语言的协程为什么长得不一样。常见分类主要看两个维度:调用栈和调度方式。
按调用栈分类:栈式协程与无栈协程
这个维度关注的是:协程挂起时,到底保存什么。
栈式协程
栈式协程,也叫有栈协程,核心特征是拥有独立调用栈,这一点很像线程。
- 工作方式:挂起时,保存的是完整调用栈;恢复时,整个调用链一起还原,可以从更深层的函数继续执行。
- 能力:可以在任意函数深度挂起,通用性很强。
- 优点:使用自然,程序员不必太关心当前挂起点在第几层调用里。
- 缺点:内存开销更高,实现也更复杂,需要处理栈分配、增长和切换。
如果要打个比方,它像是把整个演出现场完整录下来,暂停之后可以原地续播。
无栈协程
无栈协程没有独立调用栈,通常依靠状态机来保存执行进度。这也是现代 async/await 体系非常常见的实现方式。
- 工作方式:编译器把可挂起函数编译成状态机。挂起时只保存必要局部变量,以及当前执行到哪个状态,例如某个
label。 - 能力边界:挂起点通常必须显式写在协程函数中,比如
await或yield,不能在普通深层函数里随意挂起。 - 优点:内存占用极小,实现简单,尤其适合语言层和库层结合实现。
- 缺点:使用上有限制,所有可能挂起的调用链往往都要带上
async或同类标记,这就是常说的“颜色问题”或“传染性”。
这个模型更像一本带书签的分支小说:不需要记住整本书翻到哪一页,只要记住当前页码和几个关键选择即可。
按调度方式分类:对称协程与非对称协程
这个维度关注的是:协程把执行权交出去时,是直接指定下一个协程,还是回到调用者。
对称协程
- 核心特征:所有协程地位平等,任意协程都可以把执行权直接转移给另一个指定协程。
- 工作方式:类似协程 A 通过
transfer(B)直接切到协程 B。 - 优点:足够灵活,能表达复杂控制流。
- 缺点:控制流容易分散,代码理解和维护成本偏高。
它有点像圆桌会议,谁讲完都可以点名下一位继续发言。
非对称协程
- 核心特征:协程之间带有明显的调用关系,通常围绕
yield和resume展开。 - 工作方式:调用者通过
resume启动协程;协程执行到yield时,不是随意跳给别人,而是把控制权返回给恢复它的那个调用者。 - 优点:结构更接近普通函数调用,控制流更清晰,生产者-消费者等模型实现起来也更直观。
- 缺点:灵活性不如对称协程。
这也是大多数现代语言更常见的选择,包括 Kotlin 在内的很多协程体系,都更偏向这种结构化、易推理的模型。
把两种分类放在一起看
| 分类维度 | 类型 | 核心特征 | 典型代表 |
|---|---|---|---|
| 调用栈 | 栈式协程 | 有独立栈,可在任意函数深度挂起 | Go (Goroutine), C++20 |
| 无栈协程 | 通过状态机实现,挂起点受限 | JS/Python/C# (async/await), Rust | |
| 调度方式 | 对称协程 | 协程间直接、平等地切换 | 早期 Lua |
| 非对称协程 | 通过 yield/resume 与调用者交互 | 大多数现代语言 (Python生成器, JS async/await, Kotlin) |
还要注意,这两套分类是正交的,并不是二选一。现实中的协程系统,可能是:
- 栈式 + 非对称:如 Go。
- 无栈 + 非对称:如 JavaScript、Python、C# 的
async/await,也是目前最常见的组合。 - 栈式 + 对称:一些研究性或特定领域实现。
理解 Kotlin 协程时最该记住什么
如果你只想先抓住最重要的判断点,可以记住下面三件事:
- 协程不是线程替身,而是在线程之上组织异步与并发的一层抽象。
- 协程的核心价值,一是把异步代码写得更像同步代码,二是用更低成本承载大量等待中的任务。
- 协程之所以轻,关键不在“更快的线程”,而在“更多工作留在用户态完成”,从而减少系统调用、切换开销和内存浪费。
把这几个概念先理顺,再去看 Kotlin 里的 suspend、调度器、作用域和上下文,就不容易只停留在语法层理解了。







