Kotlin 协程设计思想(八):suspend 到底是什么?为什么 suspend 不是开启协程?

发布时间:2026/6/8 22:35:15

Kotlin 协程设计思想(八):suspend 到底是什么?为什么 suspend 不是开启协程? —— 从 Continuation、状态机到协程恢复机制彻底讲透 Kotlin 协程真正的底层原理前面几篇Kotlin 协程设计思想一CoroutineContext 到底是什么为什么 Job 和 Dispatcher 可以直接相加-CSDN博客Kotlin 协程设计思想二Job 到底是什么为什么协程能被取消-CSDN博客Kotlin 协程设计思想三Dispatchers 到底是什么切线程真的只是切线程吗-CSDN博客Kotlin 协程设计思想四launch、async、withContext 到底有什么区别_kotlin 协程launch和 async启动有什么区别-CSDN博客Kotlin 协程设计思想五协程异常为什么这么难理解_kotlin学习 csdn-CSDN博客Kotlin 协程设计思想六结构化并发到底是什么为什么 Google 一直强调 Scope-CSDN博客Kotlin 协程设计思想七为什么 Kotlin 要设计 SupervisorJob 和 supervisorScope-CSDN博客我们已经讲了CoroutineContext↓Job↓Dispatcher↓launch / async↓Exception↓Structured Concurrency↓Supervisor到这里很多同学会遇到一个非常容易误解的问题。例如suspend fun login() { }很多教程会说这是一个挂起函数。于是很多人脑子里就变成了suspend 开启协程包括我刚开始学协程的时候也是这么理解的。直到后来重新看 Kotlin 协程的设计才突然发现suspend 根本不是开启协程。甚至可以说suspend 什么都不会启动。今天这篇我们就彻底讲透suspend 到底是什么一、第一个误区suspend 不是开启协程例如suspend fun login() { println(login) }很多人会觉得只要写了suspend这个函数就会开启协程。实际上完全不是。如果你这样写fun main() { login() }会直接编译报错。为什么因为suspend 函数不能自己运行。它必须在协程环境中调用例如launch { login() }或者async { login() }或者runBlocking { login() }也就是说launch / async / runBlocking 这些才是提供协程环境的东西。而suspend本身并不会启动任何协程。二、如果 suspend 是启动器会发生什么我们反过来想一个问题。如果suspend 开启协程那么下面代码login() login() login()是不是应该开启三个协程显然不是。所以结论很明确suspend 根本不是协程启动器。真正负责启动协程的是launch async runBlocking而不是suspend三、那 suspend 到底是什么一句话suspend 是告诉编译器这个函数可能会挂起。注意是可能会挂起不是一定挂起。例如suspend fun login() { println(hello) }这个函数虽然加了suspend但它实际上一点都不会挂起。但是编译器允许它将来变成这样suspend fun login() { delay(1000) }这里真正发生挂起的是delay(1000)而不是login()这个名字本身。所以suspend 的本质不是启动协程而是允许这个函数内部出现挂起点。四、什么叫挂起我们先看 Java 里的写法Thread.sleep(3000);这是什么意思线程傻等 3 秒。这 3 秒里线程什么都干不了。但是 Kotlin 协程里的delay(3000)不是这样。它的特点是当前协程暂停 线程释放出去 线程可以去执行别的任务 时间到了以后 协程再恢复回来所以Thread.sleep 阻塞线程 delay 挂起协程这就是 suspend 最核心的含义暂停当前协程以后还能恢复。五、暂停之后谁负责恢复问题来了。例如println(A) delay(3000) println(B)执行过程是打印 A ↓ delay 挂起 ↓ 3 秒后 ↓ 继续打印 B那问题是3 秒后协程怎么知道要从 println(B) 继续执行答案就是Continuation六、Continuation 到底是什么一句话Continuation 就是协程的恢复器。它记录了协程挂起时的现场。例如println(A) delay(3000) println(B)当执行到delay(3000)的时候协程会暂停。暂停时Continuation 会记录现在执行到哪里了 下一步应该执行什么 局部变量是什么 恢复后应该从哪里继续所以3 秒之后协程不是重新从头执行而是通过 Continuation 恢复到原来的位置。也就是继续执行println(B)七、Continuation 很像游戏存档这个机制其实很像游戏存档。你在游戏里打 Boss。打到一半保存进度然后退出游戏。第二天再打开游戏读取存档继续从上次的位置打。Continuation 也是类似的。例如println(A) delay(1000) println(B) println(C)执行到delay时协程暂停。此时保存的信息大概是A 已经执行完了 delay 正在等待 B 还没执行 C 还没执行等恢复时不是重新执行 A而是直接从 B 开始。这就是挂起与恢复。八、Kotlin 是怎么做到的答案是编译器状态机。例如suspend fun test() { println(A) delay(1000) println(B) }编译器会把它改写成类似下面这种结构when (state) { 0 - { println(A) state 1 delay(1000) return } 1 - { println(B) } }第一次执行state 0 打印 A 执行 delay 挂起 return恢复时state 1 直接执行 B这就是协程挂起和恢复的底层核心状态机 Continuation九、原来 suspend 不是线程魔法很多人第一次理解这里时会突然发现Kotlin 协程并不是什么神秘的线程魔法。它的核心是编译器把 suspend 函数改造成状态机 Continuation 保存恢复点 挂起时退出 恢复时继续执行所以协程不是操作系统级别的新线程。它更像是编译器帮你生成了一套可以暂停、可以恢复的代码结构。十、为什么 delay 是 suspend现在再看delay(1000)它为什么是suspend因为它会暂停当前协程 等待时间结束 然后恢复执行所以它必须是挂起函数。同理job.join()为什么是suspend因为它要等待另一个协程完成。等待期间当前协程会挂起。deferred.await()为什么是suspend因为它要等待结果。等待期间当前协程会挂起。flow.collect()为什么是suspend因为它要持续等待 Flow 发射数据。等待期间当前协程可能挂起。十一、withContext 为什么也是 suspend例如withContext(Dispatchers.IO) { // IO 操作 }withContext的特点是切换 Dispatcher 执行代码块 等待代码块执行完成 再切回来继续执行这个过程本质上也需要暂停当前协程 切到新的调度器执行 执行完成后恢复所以withContext()也是suspend。十二、launch 为什么不是 suspend很多人会问既然协程都和 suspend 有关那为什么launch { }不是suspend原因很简单launch 不等待结果。它的特点是启动一个新协程 立即返回一个 Job 当前协程不需要挂起所以它不需要是suspend。同理async { }本身也不是suspend。因为 async 只是启动一个协程并立即返回Deferred真正会挂起的是deferred.await()因为await()要等待结果。十三、整个协程体系突然串起来了现在回头看这些 APIlaunch async delay join await collect withContext规律就非常清楚了。launch启动协程不等待结果不挂起 async启动协程不等待结果返回 Deferred delay等待时间挂起当前协程 join等待协程完成挂起当前协程 await等待结果挂起当前协程 collect等待 Flow 数据挂起当前协程 withContext切换上下文并等待执行完成挂起当前协程所以判断一个函数为什么是suspend核心就看一句话它是否可能暂停当前协程并在未来恢复。十四、最终理解 suspend如果让我一句话解释suspend我不会说suspend 是开启协程而会说suspend 告诉编译器这个函数可能暂停当前协程并且以后还能恢复执行。恢复靠什么Continuation实现靠什么编译器状态机所以suspend 不是线程 suspend 不是协程 suspend 不是启动器 suspend 是一种编译器机制它解决的是协程如何暂停 协程如何恢复十五、放回整个协程设计体系里看到这一篇整个协程系列的脉络就更清楚了。如果说CoroutineContext协程在哪里运行 Job协程活多久 Dispatcher协程由谁调度 Scope协程属于谁 Supervisor异常传播到哪里那么suspend协程如何暂停以及如何恢复你会发现到第八篇这个系列已经不是简单的 API 教程了。它真正回答的是Kotlin 协程为什么要这样设计这也是理解协程最重要的地方。因为协程的核心不是线程。而是挂起 恢复 状态机 Continuation真正理解了suspend你才算真正摸到了 Kotlin 协程的底层设计。十六、最终总结suspend不是开启协程。它不会创建线程。它不会启动任务。它只是告诉编译器这个函数可能会挂起。而挂起的本质是暂停当前协程 释放线程 保存现场 未来恢复实现机制是Continuation 编译器状态机所以协程真正厉害的地方不是“多开几个线程”。而是用看起来同步的代码写出可以暂停、可以恢复、不会阻塞线程的异步逻辑。这才是 suspend 的真正价值。下篇预告到这里协程系列开始进入真正的底层了。那么还有一个终极问题Flow 为什么是冷流 emit 到底发生了什么 collect 为什么一定是 suspend flowOn 为什么能切线程 StateFlow、SharedFlow 为什么建立在 Flow 之上下一篇继续Kotlin 协程设计思想九Flow 到底是什么为什么 Google 又设计了一套数据流—— 从 suspend、Channel、callbackFlow 到 StateFlow、SharedFlow彻底讲透 Kotlin Flow 的设计哲学。

相关新闻