协程的概念与 Kotlin 编译器的工作原理
大家好,我是 ONDA 后端开发工程师 Gunner(정재훈),负责酒店运营管理解决方案的开发。
今天聊聊 Kotlin 环境下协程(Coroutine)的工作原理。
协程能把基于回调的代码改写成顺序执行的样子。看例子:
// 回调版本
fun postItem(item: Item) {
requestTokenAsync { token ->
createPostAsync(token, item) { post ->
processPost(post)
}
}
}
// 协程版本
suspend fun postItem(item: Item) {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
协程让你摆脱回调地狱,代码看着也顺眼。它怎么做到的?
1. 协程与挂起点
维基百科对协程的定义:
"Coroutines are computer program components that allow execution to be suspended and resumed, …" —— 维基百科
协程是能让执行流程暂停和恢复的程序组件。
Kotlin 协程也能暂停和恢复执行 —— 暂停的位置叫 挂起点(suspension point)。在 IntelliJ IDE 里,挂起点会被标出来:

挂起点出现在调用 suspend 修饰的函数时。上图里 requestToken()、createPost()、processPost() 都是 suspend 函数。
执行到挂起点,原流程暂停,执行完 suspend 函数后再恢复。内部怎么实现的? 看例子:
suspend fun main() {
println("Before")
suspendCoroutine { continuation ->
thread {
println("Suspended")
Thread.sleep(1000)
continuation.resumeWith(Result.success(Unit))
println("Resumed")
}
}
println("After")
}
调用 suspendCoroutine 时 main() 暂停。传入的 lambda 里调用 continuation.resumeWith() 恢复原函数执行。这样挂起点暂停执行,suspend 函数跑完后,从暂停位置继续。
2. Continuation 就是回调
讲协程必提 Continuation。常见说法: "Kotlin 协程用 CPS(Continuation Passing Style) 实现暂停与恢复"。
前面例子里 suspendCoroutine 的 lambda 参数名就叫 continuation。Continuation 是老概念,Scheme 语言里用 call/cc 函数(call-with-current-continuation)操作它。
Continuation 让你暂停原函数,suspend 函数执行完后恢复原函数。它装了什么能做到这些?
工作中途暂停去干别的事,完事后想接着干,得记住:
- 进度到哪了(该从哪接着干)
- 暂停时的环境(比如打开的 Excel 文件和参考资料)
这两样齐了才能继续。
函数暂停与恢复也一样,需要:
- 执行到哪一步了
- 暂停时的上下文变量值(比如 varA = 10, varB = "This is string")
把这些信息装进 continuation,暂停时保存,恢复时取出来用,原函数就能继续干。
听着耳熟? 对,回调(callback)。
回调是传一个 lambda,在特定时刻执行。Continuation 也一样 —— 只是执行时机不同:
- Continuation: lambda 立即执行,原流程暂停,lambda 执行完后原流程恢复
- Callback: 原流程立即继续,lambda 在某个时刻才执行
执行时机不同,本质上回调和 continuation 是一回事。
3. Kotlin 编译器自动转换
前面讲了挂起点和 continuation。谁来做这些处理?
Kotlin 编译器。编译器看到 suspend 修饰的函数,会把代码改成:
// Kotlin
suspend fun createPost(token: Token, item: Item): Post { ... }
// 编译器转成等价的 Java 代码
// Java/JVM
Object createPost(Token token, Item item, Continuation<Post> cont) { ... }
Continuation 接口长这样:
// Kotlin ~v1.2
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
// Kotlin v1.3~
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
调用 resumeWith 传结果值,恢复原函数执行。但每个挂起点都要暂停与恢复,怎么记住执行到哪了?
Kotlin 用 标签(label) 标记。例子:
suspend fun requestToken() { ... }
suspend fun createPost(token, item) { ... }
suspend fun processPost(post) { ... }
suspend fun postItem(item: Item) {
// LABEL 0
val token = requestToken()
// LABEL 1
val post = createPost(token, item)
// LABEL 2
processPost(post)
}
每个挂起点分配一个 label。记住 label 编号,恢复时从对应 label 继续:
suspend fun postItem(item: Item) {
switch (label) {
case 0:
val token = requestToken()
case 1:
val post = createPost(token, item)
case 2:
processPost(post)
}
}
Label 信息存哪? 前面说了,在 continuation 里。用 continuation 里的 label 做分支,从暂停处恢复:
fun postItem(item: Item, cont: Continuation) {
val sm = object : CoroutineImpl { ... }
when (sm.label) {
0 -> val token = requestToken(sm)
1 -> val post = createPost(token, item, sm)
2 -> processPost(post)
}
}
有点眉目了。首次执行时创建 continuation,后续挂起点复用它。
但还缺东西: label 保存在哪? label 为 1 时调用 createPost 要用的 item 值从哪来? 执行到那行时用 resumeWith 恢复,原始 item 参数不在了吧?
对。所以还得把状态(state)也存进 continuation,用时取出来:
fun postItem(item: Item, cont: Continuation) {
val sm = object : CoroutineImpl { ... }
when (sm.label) {
0 -> {
sm.item = item
sm.label = 1
val token = requestToken(sm)
}
1 -> {
val item = sm.item
val token = sm.result as Token
sm.label = 2
val post = createPost(token, item, sm)
}
2 -> processPost(post)
}
}
挂起点记录、状态管理由 suspendCoroutine() 和 continuation 负责。Kotlin 编译器处理 suspend 修饰符时,自动改写代码。
实际编译出的字节码反编译后是复杂的 Java 代码,这里用 Kotlin 伪代码演示核心逻辑 —— 大体思路是这样。
4. 协程对 Future-like 类型的扩展支持
前面例子展示了如何把回调风格改成顺序风格。但这样写性能不会提升,回调也没啥大问题,除了风格差异还有别的优势吗?
协程的优势是高效利用闲置线程。挂起点暂停原任务,在特定线程池执行其他任务,原线程空闲出来给别的工作用。
多个 async 调用同时发出一起处理时,如果都是 suspend 调用,线程利用效率高。还有一个好处: JVM 的各种 async 库都有自己的 Future-like 类型:
- Guava: ListenableFuture
- RxJava: Observable
- JDK8: CompletableFuture
- …
类不同,但基本原理跟 Future 一样。Kotlin 协程提供了与这些 Future-like 类型互操作的扩展代码。
只要库有对应扩展,就能用统一方式取值 —— 比如协程里用 await(),不管对象是 ListenableFuture、CompletableFuture、Observable 还是 Promise,都一个写法。这通过 Kotlin 扩展(extension)实现,协程让你用相同方式对接不同库:
// 用 Guava 的 Java 函数
public ListenableFuture<Image> guavaLoadImageAsync(String name) { … }
// 用 RxJava 的 Java 函数
public Observable<Image> rjLoadImageAsync(String name) { … }
// 合并两张图的 Java 函数
public Image combineImage(Image image1, Image image2) { … }
// Kotlin 里用协程调用
// 获取两张图,返回 CompletableFuture
fun combineImagesAsync(name1: String, name2: String): CompletableFuture<Image> = future {
val future1 = guavaLoadImageAsync(name1)
val future2 = rjLoadImageAsync(name2)
combineImages(future1.await(), future2.await())
}
💡 Kotlin 编译器总结
-
Kotlin 编译器转换带 suspend 修饰符的函数。
-
转换后的函数用 continuation(像回调一样)管理挂起点和状态,实现暂停/恢复。
-
暂停的原线程可以干别的活,效率更高。
-
协程提供的 Future-like 扩展,让你用统一写法操作不同 Future-like 库。
今天介绍了协程概念和 Kotlin 编译器的工作原理。多亏这些 Future-like 扩展,协程能用一致的代码风格对接各种库。谢谢。