专栏模块:生存指南 本文将带你走进协程的"组织部",揭秘协程是如何从零创建的,结构化并发是如何编织成网的,以及上下文参数在不同场景下的妙用。
引言
如果说 suspend 是协程的灵魂,那么结构化并发 (Structured Concurrency) 就是它的骨架。在 Java 中,线程像是一匹脱缰的野马;而在 Kotlin 中,协程更像是纪律严明的军队。本文将带你从源码和设计逻辑角度,彻底搞懂协程的"生老病死"。
1. 协程是怎么诞生的?
要创建一个协程,通常有三个要素:Scope (作用域) 、Context (上下文) 和 Builder (构建器)。
1.1 构建器 (Builders):launch vs async
launch:源码中返回一个StandaloneCoroutine。它是"发射后不管"的模式,不关心执行结果,只返回一个可控制生命周期的Job。async:源码中返回一个DeferredCoroutine。它继承自Job,但多了一个await()挂起函数,用于在未来某个时刻获取结果。
1.2 作用域 (Scope):协程的"大本营"
CoroutineScope 接口极其简单,只持有一个 coroutineContext。 但它的意义非凡:它是协程生命周期的界限 。当你调用 scope.launch 时,新协程会自动继承该 Scope 的上下文,并将其 Job 注册为 Scope 的 Job 的子项。
GlobalScope:生命周期随进程,容易造成内存泄漏,不建议使用。MainScope():绑定主线程,常用于 Activity/Fragment。customScope:通过CoroutineScope(context)自定义,用于特定的后台任务模块。viewModelScope:Android 特有,绑定 ViewModel 生命周期。
1.3 上下文 (CoroutineContext):协程的"基因"
上下文是协程执行环境的集合。在源码中,实际上是一个以 Element 为元素的集合。 它类似于一个 类型安全的 Map,常见的"基因"有四种(Element的具体实现):
- Job (生命周期基因) :
Job():普通 Job,一个子协程失败会导致父协程及其他子协程全部取消。SupervisorJob():监督 Job,子协程失败不影响父协程。
- ContinuationInterceptor (调度基因) :
- 即
Dispatchers。它决定了协程在哪个线程池运行(Main, IO, Default, Unconfined)。
- 即
- CoroutineExceptionHandler (异常处理基因) :
- 类似于全局的
UncaughtExceptionHandler,用于捕获 launch 抛出的顶层异常。
- 类似于全局的
- CoroutineName (身份基因) :
- 用于调试时给协程命名。
特别说明:
- 上下文可以通过
+运算符进行组合。CombinedContext它在内部通过类似链表 的形式组织数据:CombinedContext(left, element)。它持有左侧的上下文和右侧的新元素,这种设计保证了上下文的不可变性。
kotlin
val context = Dispatchers.IO + Job() + CoroutineName("MyTask")
- 所有的
CoroutineContext最终都可以展开为一个以EmptyCoroutineContext为底、以多个Element为节点的单链表。
2. 结构化并发:如何"编织"成网?
结构化并发的核心在于:父子关系的确立。
2.1 源码背后的父子绑定
当你调用 scope.launch(context) 时,内部发生了精妙的合并逻辑:
- Context 合并 :
newContext = scope.context + contextArgument。 - 创建协程对象 :例如
StandaloneCoroutine(newContext)。 - 确立父 Job :从
newContext中提取出parentJob。->parentContext[Job] - 双向关联 :新协程启动时,会调用
parentJob.attachChild(childJob)。父 Job 会把子 Job 存入一个内部的链表结构。
2.2 结构化并发的三大特性
- 生命周期继承:子协程默认继承父协程的调度器和异常处理器。
- 协作式等待 :父协程只有在所有子协程都完成后,才会真正进入
Completed状态。 - 取消的级联传导:这是最强大的一点。父协程取消,子协程必取消。子协程发生未捕获异常,会默认导致父协程及兄弟协程全部取消。
2.3 深度解析:到底如何"编织"成网?
想象一张网,节点是 Job,连线是父子关系。
- 入网 :每个新开启的协程都会通过
attachChild将自己挂在现有的树形结构上。 - 状态同步 :当一个子 Job 抛出异常,它会立即改变自身状态并通知父 Job。父 Job 收到信号后,会开启"大清洗"模式:
- 取消所有其他正在运行的子 Job。
- 向上继续传递异常(除非是
SupervisorJob)。
- 层层保护 :这种"网状"结构确保了:一旦根节点(比如
viewModelScope)被切断,整张网上的数千个异步任务都会在瞬间被正确回收。这就是为什么 Kotlin 协程比 RxJava 或原生 Thread 更安全的原因。
3. 上下文 (CoroutineContext) 的场景化妙用
CoroutineContext 像是一个 Map,里面装载了协程的各种"属性"。
| 元素 | 作用 | 常见使用场景 |
|---|---|---|
Job |
控制生命周期 | Job() 用于开启独立作用域;SupervisorJob() 用于实现异常隔离。 |
Dispatchers |
决定运行线程 | IO 用于网络/数据库;Main 用于更新 UI;Default 用于复杂计算;Unconfined用于挖坑,最好别用 |
CoroutineExceptionHandler |
捕获顶层未捕获异常 | 用于在 launch 的最外层统一收集崩溃信息,记录日志。 |
CoroutineName |
协程命名 | 调试利器,开启 kotlinx.coroutines.debug 后在日志中清晰分辨不同任务。 |
场景示例:自定义 Scope 处理并发
kotlin
// 为某个特定的后台模块创建作用域
val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("ModuleA"))
fun doWork() {
moduleScope.launch {
// 这里的任务即使抛出异常,也不会导致 moduleScope 被关闭,
// 因为我们用了 SupervisorJob。
println("Doing work...")
}
}
4. 异常处理的"血缘"陷阱
这是结构化并发中最容易踩坑的地方。
4.1 为什么 try-catch 包不住 launch 里的异常?
kotlin
// ❌ 错误做法:包不住
try {
scope.launch { throw Exception("Failed") }
} catch(e: Exception) {
// 这里的 catch 无法捕获 launch 内部的异常
}
源码分析 :launch 开启的是异步任务,它会立即返回一个 Job 对象,而内部的抛错发生在未来。此时原函数的 try-catch 早就执行完了。
4.2 正确姿势:CoroutineExceptionHandler 与 SupervisorJob
如果你希望实现:任务 A 崩了,不影响任务 B。
- 必须使用
SupervisorJob或supervisorScope。 - 源码原理 :
SupervisorJob覆写了childCancelled,当子协程抛错时,它会返回false,告诉父级:"我处理好了,别取消我,也别取消其他兄弟"。
为了优雅地处理异常,我们需要组合使用这两种工具。
第一步:定义异常处理器
CoroutineExceptionHandler 只有在协程顶层(直接子协程)发生未捕获异常时才会被触发。
kotlin
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获到异常: ${exception.message}")
}
第二步:设置隔离带 (SupervisorJob)
如果不使用 SupervisorJob,任何一个子协程崩了都会取消整个作用域。
kotlin
// 组合使用:隔离异常 + 集中处理
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main + handler)
fun fetchAll() {
scope.launch {
// 任务 1 崩溃,由于 handler 的存在,APP 不会闪退
// 由于 SupervisorJob 的存在,任务 2 依然可以继续执行
throw RuntimeException("Task 1 failed")
}
scope.launch {
println("Task 2 正常执行")
}
}
关键规则总结:
- 位置很重要 :
CoroutineExceptionHandler必须设置在CoroutineScope的上下文中,或者作为launch的参数传入最外层协程。 - SupervisorJob 的必要性 :没有它,异常会沿着树结构向上蔓延,直到取消整棵 Job 树。它覆写了
childCancelled返回false,从而切断了这种蔓延。
4.3 应用场景:生命周期感知
在 Android 中,最经典的应用就是 viewModelScope。
- 当 ViewModel 被清除时,它持有的
SupervisorJob会被取消。 - 这从根源上解决了网络回调导致的 Activity 泄漏或空指针问题。
5. 总结
- Scope 决定了你在哪工作。
- Context 决定了你怎么工作。
- 结构化并发 确保了你不会因为工作出意外而导致整个系统崩溃。
掌握了这一层,你就从"会用协程"进化到了"能设计鲁棒性架构"的阶段。
下一篇预告:《Kotlin 协程深度解析(三):流式编程------Flow 的响应式进化》