协程的父子关系是 Kotlin 协程里非常重要的概念,核心叫:
结构化并发 Structured Concurrency
可以简单理解为:
一个
CoroutineScope里面启动的协程,会归这个 Scope 管。
一个协程里面再启动新的协程,新的协程默认就是它的子协程。
父协程会等待子协程结束;父协程取消,子协程也会取消。
1. 最简单的父子关系
看这个例子:
viewModelScope.launch {
launch {
delay(1000)
println("子协程 1 完成")
}
launch {
delay(2000)
println("子协程 2 完成")
}
println("父协程代码执行到这里")
}
结构大概是:
viewModelScope
└── 父协程 launch
├── 子协程 1 launch
└── 子协程 2 launch
注意一点:
println("父协程代码执行到这里")
可能会很快打印,但父协程不会真正结束,它会等待两个子协程都执行完。
2. 父协程会等待子协程
例子:
viewModelScope.launch {
launch {
delay(2000)
println("子协程完成")
}
println("父协程代码结束")
}
输出类似:
父协程代码结束
子协程完成
虽然父协程里的代码先执行完了,但父协程的 Job 还没有真正完成,因为它要等子协程完成。
你可以理解成:
父协程 = 自己的代码 + 所有子协程
只有这些全部结束,父协程才算结束。
3. 父协程取消,子协程也取消
比如:
val job = viewModelScope.launch {
launch {
repeat(10) {
delay(500)
println("子协程执行中 $it")
}
}
}
delay(1200)
job.cancel()
当你取消父协程:
job.cancel()
里面的子协程也会被取消。
结构:
父协程 cancel
↓
所有子协程一起 cancel
这就是为什么 Android 里:
viewModelScope.launch { }
当 ViewModel 销毁时,里面的任务会自动取消。
因为:
ViewModel cleared
↓
viewModelScope 被取消
↓
viewModelScope 下所有协程全部取消
4. 子协程异常,默认会影响父协程
默认情况下:
一个普通子协程抛异常,会导致父协程取消,然后父协程会取消其它子协程。
例如:
viewModelScope.launch {
launch {
delay(500)
throw RuntimeException("子协程 1 出错了")
}
launch {
repeat(10) {
delay(300)
println("子协程 2 执行中 $it")
}
}
}
当子协程 1 抛异常:
子协程 1 异常
↓
父协程被取消
↓
子协程 2 也被取消
这就是普通协程的默认行为。
5. SupervisorJob:子协程之间互不影响
有时候你希望:
一个子任务失败,不要影响其它子任务。
这时可以用 supervisorScope 或 SupervisorJob。
推荐方式:supervisorScope
viewModelScope.launch {
supervisorScope {
launch {
delay(500)
throw RuntimeException("任务 1 失败")
}
launch {
repeat(5) {
delay(300)
println("任务 2 继续执行 $it")
}
}
}
}
这时:
任务 1 失败
任务 2 不会被取消
结构变成:
viewModelScope
└── 父协程
└── supervisorScope
├── 任务 1 失败
└── 任务 2 继续
6. 普通父子关系 vs Supervisor
普通 Job
父协程
├── 子协程 A 失败
└── 子协程 B 被取消
默认是:
一个孩子出事,全家取消
SupervisorJob / supervisorScope
父协程
├── 子协程 A 失败
└── 子协程 B 继续
可以理解为:
一个孩子出事,不影响其他孩子
7. launch 和 async 的父子关系
launch 和 async 都会创建子协程。
launch
viewModelScope.launch {
val job = launch {
delay(1000)
println("launch 子协程")
}
}
launch 返回的是:
Job
它主要用于“不需要返回结果”的任务。
async
viewModelScope.launch {
val deferred = async {
delay(1000)
"用户数据"
}
val result = deferred.await()
println(result)
}
async 返回的是:
Deferred<T>
它用于“需要返回结果”的任务。
结构:
父协程 launch
└── 子协程 async
8. async 的异常要注意
例如:
viewModelScope.launch {
val deferred = async {
throw RuntimeException("async 出错")
}
deferred.await()
}
async 的异常通常会在:
deferred.await()
这里暴露出来。
所以一般要这样处理:
viewModelScope.launch {
try {
val deferred = async {
repository.getUser()
}
val user = deferred.await()
} catch (e: Exception) {
// 处理异常
}
}
9. coroutineScope:创建一个新的子作用域
coroutineScope 会创建一个新的协程作用域,并且等待里面所有子协程完成。
viewModelScope.launch {
coroutineScope {
launch {
delay(1000)
println("任务 1")
}
launch {
delay(2000)
println("任务 2")
}
}
println("两个任务都完成后,才执行这里")
}
输出:
任务 1
任务 2
两个任务都完成后,才执行这里
结构:
viewModelScope
└── launch
└── coroutineScope
├── launch 任务1
└── launch 任务2
coroutineScope 的特点:
会等待所有子协程完成
只要一个子协程失败,其它子协程会被取消
10. supervisorScope:失败隔离的子作用域
viewModelScope.launch {
supervisorScope {
launch {
throw RuntimeException("任务 1 失败")
}
launch {
delay(1000)
println("任务 2 仍然执行")
}
}
}
supervisorScope 的特点:
会等待所有子协程完成
某个子协程失败,不会自动取消其它子协程
不过要注意,失败的子协程异常还是要处理,否则可能会打印崩溃日志。
建议:
viewModelScope.launch {
supervisorScope {
launch {
try {
repository.loadA()
} catch (e: Exception) {
// 单独处理任务 A 异常
}
}
launch {
try {
repository.loadB()
} catch (e: Exception) {
// 单独处理任务 B 异常
}
}
}
}
11. withContext 也是父子关系吗?
withContext 不像 launch 那样启动一个并发的新任务,它更像是:
切换上下文执行一段代码,并等待结果返回。
例如:
viewModelScope.launch {
val user = withContext(Dispatchers.IO) {
repository.getUser()
}
_user.value = user
}
结构上它仍然属于当前协程的一部分。
如果外层协程取消,withContext 里的代码也会取消。
viewModelScope
└── launch
└── withContext(IO)
但它不是那种“并发跑多个子任务”的感觉,更像是当前协程切换到 IO 去执行一段。
12. Android 里的父子关系
viewModelScope
class UserViewModel : ViewModel() {
fun loadUser() {
viewModelScope.launch {
val user = repository.getUser()
}
}
}
结构:
ViewModel
└── viewModelScope
└── loadUser 的协程
当 ViewModel 销毁:
ViewModel.onCleared()
↓
viewModelScope 取消
↓
里面所有协程取消
lifecycleScope
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// 更新 UI
}
}
}
结构:
Activity / Fragment
└── lifecycleScope
└── 收集 Flow 的协程
当 Activity/Fragment 销毁:
Lifecycle destroyed
↓
lifecycleScope 取消
↓
里面所有协程取消
13. 不推荐 GlobalScope
比如:
GlobalScope.launch {
repository.getUser()
}
这个协程没有跟 Activity、Fragment、ViewModel 绑定。
也就是说:
页面销毁了,它可能还在跑
ViewModel 销毁了,它可能还在跑
容易导致:
- 内存泄漏
- 无效回调
- 难以取消
- 生命周期混乱
所以 Android 里一般用:
viewModelScope.launch { }
或者:
lifecycleScope.launch { }
而不是:
GlobalScope.launch { }
14. 常见代码对比
串行执行
viewModelScope.launch {
val user = repository.getUser()
val orders = repository.getOrders()
}
执行顺序:
先 getUser
再 getOrders
并行执行
viewModelScope.launch {
val userDeferred = async {
repository.getUser()
}
val ordersDeferred = async {
repository.getOrders()
}
val user = userDeferred.await()
val orders = ordersDeferred.await()
}
执行顺序:
getUser 和 getOrders 同时开始
await 等待两个结果
结构:
viewModelScope
└── launch 父协程
├── async 获取用户
└── async 获取订单
15. 最重要的规则总结
你可以记这几条:
1. CoroutineScope 管理协程生命周期
2. 在一个协程里 launch/async 出来的协程,默认是它的子协程
3. 父协程取消,子协程全部取消
4. 普通 Job 下,子协程异常会取消父协程,进而取消其它兄弟协程
5. coroutineScope:一个失败,整体失败
6. supervisorScope / SupervisorJob:一个失败,不影响其它子协程
7. viewModelScope 跟 ViewModel 生命周期绑定
8. lifecycleScope 跟 Activity/Fragment 生命周期绑定
9. 不推荐随便用 GlobalScope
一句话理解
协程的父子关系就是:
任务要有归属,父任务负责管理子任务。父取消,子取消;普通情况下子失败,父也失败。
在 Android 里:
viewModelScope / lifecycleScope
就是最外层的“家长”
launch / async
就是这个家长下面创建出来的“孩子任务”