Appearance
Kotlin 协程挂起恢复与并发
一句话定义
Kotlin 协程是跑在线程池之上的可挂起、可恢复的任务(不是 OS 线程):你用顺序的 suspend 代码写异步;编译器把它改写成带 label 的状态机,用 Continuation 把局部变量和「下一条该执行的语句」存进对象;运行时(kotlinx.coroutines)在挂起点释放线程,完成后再 resume,并由 Dispatcher 决定接着在哪个线程执行。
| 层面 | 一句话 |
|---|---|
| 编译期 | suspend → 状态机 + Continuation:异步逻辑不再占满整条线程栈,断点保存在堆里 |
| 运行期 | 挂起 = 任务让出工人;恢复 = 调度器把任务接回某条线程继续跑 |
| 与线程 | 多协程可复用少量线程;同一条协程在 withContext(IO) 等切换后,生命周期里也可能先后跑在不同线程上 |
可运行验证:coroutine-execution-flow(Main → IO → Main 线程切换)
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
Java volatile / Atomic* / synchronized(对照) | volatile-vs-atomic-vs-lock | VolatileVsAtomicVsLock.java |
| Java 并发总览 | Java 并发模型总览 | — |
| Kotlin 协程执行流(Main → IO → Main) | coroutine-execution-flow | CoroutineExecutionFlowDemo.kt / AdvancedCoroutinePatternsDemo.kt(async/await / Default / supervisorScope / timeout / semaphore) / ManualCoroutineEquivalent.kt |
| Kotlin 协程 Mutex / StateFlow | coroutine-mutex-stateflow · Mutex 与 Job / Deferred 专题 | CoroutineMutexStateFlowDemo.kt / MutexDemo.kt |
前置对照:CAS 基础 · Java volatile / 锁 §3~§4
为什么需要(与 Java 原生并发对比)
在传统 Java / Android 并发里:
| 痛点 | 典型写法 | 后果 |
|---|---|---|
| 阻塞线程 | Thread.sleep()、Future.get()、锁内等 IO | 占住 OS 线程;主线程上易 ANR |
| 回调嵌套 | Retrofit enqueue、多层 Callback | 可读性差、取消与错误路径难统一 |
| 生命周期失控 | new Thread { ... }、无父任务的 GlobalScope | 页面销毁后仍跑、泄漏与竞态 |
协程要解决的核心是:异步等待时不白白占着线程,并用 结构化并发(CoroutineScope / Job)管理任务树。
需要澄清的误区(对应下文各节):
- 挂起 ≠ 一定不占 CPU:在
Dispatchers.Main上跑重计算仍会卡 UI;挂起的是「等网络/等 delay」这类可协作让出线程的点。 StateFlow/Mutex不是volatile/synchronized的 1:1 替换:语义不同,是协程世界里的状态广播与挂起互斥,Java 原语在 JNI、旧库、非协程代码里仍常见。
形象理解:服务员模型(挂起 vs 阻塞)
text
阻塞(Thread.sleep / synchronized 等锁):
服务员(OS 线程)站在桌前干等客人吃完 → 这一桌期间不能服务别人
挂起(suspend):
客人说「菜还没好」(网络未返回)
→ 服务员记下「这桌挂起、回来继续」(Continuation)
→ 先去服务其他桌(线程去跑别的协程或 UI)
→ 厨房好了再回来接着上菜(resume)协程是任务;线程是执行协程的工人。一个工人可以先后服务很多「挂起过的桌」(多协程复用少量线程)。
底层机制:协程如何实现挂起 / 恢复
1. 编译器:把 suspend 变成状态机
你写的:
kotlin
suspend fun loadUser(): User {
val raw = api.fetchUser() // 挂起点 1
return parse(raw) // 挂起点 2(若 parse 也是 suspend)
}编译后大致等价于(概念模型,非真实字节码):
kotlin
// 编译器生成带 label 的状态机 + Continuation 参数
fun loadUser(cont: Continuation<User>): Any? {
when (cont.label) {
0 -> { cont.label = 1; api.fetchUser(cont) ; return COROUTINE_SUSPENDED }
1 -> { val raw = cont.result; ...; cont.label = 2; ... }
2 -> { return parseResult }
}
}| 概念 | 含义 |
|---|---|
suspend | 标记函数可被挂起;只能在协程或其它 suspend 里调用 |
Continuation | 保存「挂起后从哪条语句、哪些局部变量继续」的续体 |
COROUTINE_SUSPENDED | 告诉运行时:线程可以走了,完成时再 resume |
状态机 label | 第几次挂起就跳到对应 case,避免回调地狱 |
所以:协程不是新线程,而是带保存点的可恢复任务;线程在挂起点被调度器拿去干别的。
2. 运行时:Dispatcher + 线程池
Dispatcher | 典型用途 | 底层 |
|---|---|---|
Main | UI 更新、轻量逻辑 | Android 主线程 Handler |
IO | 网络、磁盘、数据库 | 共享线程池(可阻塞型任务也放这里,而不是 Main) |
Default | CPU 密集计算 | 线程数 ≈ CPU 核数 |
kotlin
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { api.fetch() } // 切到 IO 线程执行
_uiState.value = data // 回到 scope 默认(常是 Main)
}withContext / launch 负责在哪个线程执行哪一段;suspend 负责等的时候不占着错误的那条线程。
3. 结构化并发:Scope + Job
text
viewModelScope(父 Job)
├── launch { loadA() }
└── launch { loadB() }
ViewModel.onCleared() → scope 取消 → 子协程一并取消对比 Java:GlobalScope / 裸 Thread 像「离职员工还在帮公司打电话」;viewModelScope / lifecycleScope 把任务绑在生命周期上,是 Android 上减少泄漏的关键。
核心概念速查(需要单独认识,不能一笔带过)
suspend / launch / async
| API | 作用 |
|---|---|
suspend fun | 可被挂起的函数;内部可调用其它 suspend API |
launch | 启动协程,不关心返回值(fire-and-forget),异常走 CoroutineExceptionHandler |
async | 启动协程并返回 Deferred<T>,用 await() 拿结果(类似 Future,但在协程里挂起等待) |
Mutex(协程互斥锁)
对应 Lab:coroutine-mutex-stateflow · Mutex 与 Job / Deferred 专题 · CoroutineMutexStateFlowDemo.kt §2 · MutexDemo.kt
保护协程临界区;拿不到锁时 suspend 当前协程,不阻塞 OS 线程(与 synchronized 在等锁时阻塞线程不同)。
kotlin
private val mutex = Mutex()
private var cache = emptyList<User>()
suspend fun updateCache(users: List<User>) {
mutex.withLock {
cache = users // 多字段一致变更放在锁内
}
}| 对比 | synchronized | Mutex |
|---|---|---|
| 等待锁时 | 阻塞线程 | 挂起协程,线程可跑别的协程 |
| 可重入 | 是(同线程可再进) | 否(同协程再 lock 会死锁)— 见 MUTEX_AND_JOB.md §2 |
| 使用场景 | 任意 Java/Kotlin 代码 | suspend 函数内的共享状态 |
| Android 主线程 | 锁内慢操作 → ANR 风险 | 仍要避免在 Main 上写长临界区;只是等锁方式不同 |
StateFlow / SharedFlow(热流,状态广播)
对应 Lab:coroutine-mutex-stateflow · §3~§4
| 类型 | 特点 | 典型用途 |
|---|---|---|
StateFlow<T> | 必有当前值;新订阅者立刻收到最新快照;value 读当前态 | UI 单源状态:UiState、加载中/成功/失败 |
SharedFlow<T> | 无强制「当前值」;可配置 replay;适合事件 | 一次性事件:Snackbar、导航、点击脉冲 |
kotlin
data class UiState(val loading: Boolean = false, val users: List<User> = emptyList())
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
suspend fun refresh() {
_uiState.update { it.copy(loading = true) }
val users = withContext(Dispatchers.IO) { repo.load() }
_uiState.update { it.copy(loading = false, users = users) }
}和 Java 对照:
| Java / 旧 Android | Kotlin 协程常见替代 | 说明 |
|---|---|---|
volatile 标志位 | Job.isActive、取消协作 | 取消是协程结构化能力,不是简单布尔可见性 |
AtomicInteger 计数 | 仍可用 AtomicInteger;或 StateFlow.update 广播 | 要「UI 也要看见」时用 Flow |
synchronized 多字段 | Mutex.withLock + 不可变 copy | 状态对外暴露快照,减少半初始化可见 |
LiveData | StateFlow + repeatOnLifecycle | Flow 更利于组合、背压与测试 |
不是说 volatile 消失了:JNI、旧 SDK、@Volatile 字段在纯 JVM 模块里仍合理。
withContext vs launch
想先把最小执行链打通,可先看 Lab:coroutine-execution-flow —— 只围绕
suspend fun showUser() { val user = withContext(IO) { repo.loadUser() }; render(user) }讲清Main → IO → Main、状态机分支、数据如何经 continuation/result 流转,以及类 Java 的 Executor + 状态机等效写法。
withContext(Dispatchers.IO) { }:挂起当前协程,在指定调度器执行块,结束后回到原上下文;适合顺序 IO。launch:新子协程并行;要注意异常传播与join。
并发互斥与状态管理选型(汇总表)
| 需求场景 | 传统 Java / JVM | Kotlin 协程方案 | 要点 |
|---|---|---|---|
| 取消 / 是否仍运行 | 手动 volatile + 检查 | coroutineContext.isActive、Job.cancel() | 结构化取消向下传播 |
| 单变量原子累加 | AtomicInteger | AtomicInteger 或 StateFlow.update | 仅计数用 Atomic 即可;要驱动 UI 用 Flow |
| 多字段一致修改 | synchronized / Lock | Mutex.withLock + 不可变 data class | 协程临界区优先 Mutex |
| UI 状态订阅 | LiveData / 手写观察者 | StateFlow + collect | 配合 lifecycleScope / repeatOnLifecycle |
| 一次性 UI 事件 | SingleLiveEvent 等 hack | SharedFlow / Channel | 避免旋转屏重复消费 |
Android 实战注意
- 主线程:UI 只能在
Main更新;IO 用withContext(IO)或flowOn(IO)。 - Scope:
viewModelScope/lifecycleScope,避免GlobalScope。 collect生命周期:用repeatOnLifecycle(STARTED),避免后台无效 collect 与泄漏。- 不要用 Flow 替代所有锁:与 Java 模块、Room 事务、第三方回调交界仍可能要用 synchronized / 平台 API。
- 协程 ≠ 免费并行:CPU 密集任务应用
Default并限制并发度,否则仍抢 CPU。
常见坑
| 坑 | 后果 | 方向 |
|---|---|---|
在 Main 上 delay 以外的重计算 | 卡顿 / ANR | withContext(Default) |
GlobalScope.launch | 泄漏、无法取消 | 绑定 viewModelScope |
把 StateFlow 当 volatile | 误用 API | 状态用不可变快照 + update |
SharedFlow 当 StateFlow 显示 UI | 旋转屏丢状态 | 界面态用 StateFlow |
async 异常未 await | 静默失败 | supervisorScope + 统一处理 |
Mutex 锁内调用 Main 慢操作 | 逻辑仍慢 | 锁内只改内存,IO 在锁外 |
与相近概念对比
| 概念 | 关系 |
|---|---|
Java Future | async/await 类似,但协程挂起不阻塞线程 |
| RxJava | 同为响应式;协程 + Flow 更贴近 Kotlin 语法 |
Dart Future / async-await | 同属挂起续体模型;Dart 单线程 event loop,Kotlin 多线程 Dispatcher |
Flutter Isolate | 内存隔离;Kotlin 默认协程共享内存,需 Mutex / 不可变状态 |
对应实验
| Lab | 说明 | 状态 |
|---|---|---|
| volatile-vs-atomic-vs-lock | Java 原语对照 | 已有 |
| coroutine-execution-flow | Main → IO → Main 执行链 + async/await / Default / supervisorScope / 超时降级 / 并发限制 / Kotlin 手写状态机等效实现 | 已有 |
| coroutine-mutex-stateflow | Mutex + StateFlow + SharedFlow 可运行示例 | 已有 |
复习检查题
协程「挂起」和线程「阻塞」最大区别是什么?
答:挂起时协程任务让出执行权,OS 线程可去跑其它协程;阻塞时线程本身卡在等待上。协程是轻量任务,线程是执行载体。
suspend函数在编译后大致变成什么结构?答:带
Continuation参数的状态机(label标记挂起点),返回COROUTINE_SUSPENDED表示已挂起,完成后通过resume从断点继续。StateFlow和SharedFlow各适合什么?答:
StateFlow持有当前状态快照,适合 UI 界面态(加载中/列表数据)。SharedFlow适合事件流(Toast、导航、一次性信号),默认不保证新订阅者拿到历史「当前值」。为什么协程里推荐
Mutex而不是在主流程里用synchronized?答:等
synchronized会阻塞线程;Mutex在协程中等待时挂起协程,线程可复用。在 Android 上减少主线程或池线程被无谓占满的风险(锁内仍不能做长 IO)。Mutex和synchronized在「重入」上有什么不同?答:
synchronized/ReentrantLock可重入(同线程可再进同一把锁)。Mutex不可重入——同协程在withLock内再次withLock同一实例会挂起自己、永远等不到锁,形成死锁。分层 API 应「一层锁」或拆*Locked()无锁内层方法。详见 MUTEX_AND_JOB.md §2。Dispatchers.Main/IO/Default各放什么任务?答:
Main:UI 与轻量逻辑;IO:网络、磁盘、阻塞 IO;Default:CPU 密集计算。重活放错 Dispatcher 仍会卡 UI 或浪费线程。用了协程是否就不需要
volatile/AtomicInteger?答:不是。协程解决异步结构与线程占用;跨线程单变量原子、Java 互操作、非 suspend 代码仍可能需要
volatile/Atomic*。UI 状态更常交给StateFlow+ 不可变对象。
速记
- 协程 = 可挂起恢复的任务;线程 = 跑任务的工人;suspend = 编译器状态机 + Continuation。
- 挂起释放线程,阻塞占住线程;Main 上重 CPU 仍会卡。
- UI 态 → StateFlow;一次性事件 → SharedFlow;协程临界区 → Mutex.withLock(不可重入,勿嵌套同实例 withLock)。
- Scope 绑生命周期;IO 用 withContext(IO);Java 对照 → volatile-vs-atomic-vs-lock;可运行 → coroutine-mutex-stateflow。