结构化并发
结构化并发(Structured Concurrency)是 Kotlin 协程的设计基石。它不仅仅是一种父子层级关系,更是一种确保协程任务生命周期确定性、资源不泄露以及错误可传播的编程范式。
核心定义与 API 契约
在结构化并发中,新的协程只能在特定的 CoroutineScope 中启动。该作用域划定了协程的生命周期范围,确保所有子协程在父作用域结束前必须完成。
CoroutineScope 签名
查看 CoroutineScope 定义
kotlin
public interface CoroutineScope {
/**
* 该作用域的上下文。
* 结构化并发要求上下文必须包含一个 Job 用于管理生命周期。
*/
public val coroutineContext: CoroutineContext
}Job 的分层契约
Job 是结构化并发的物理句柄。当你在一个 CoroutineScope 中调用 launch 或 async 时,新协程的 Job 会自动成为父作用域 Job 的子项,形成一棵 Job 树。
- 确定性完成:父 Job 只有在所有子 Job 均进入
Completed或Cancelled状态后,才能结束自己的Completing状态。 - 取消传播:父 Job 被取消时,会递归地取消其所有子 Job。
- 异常连坐:(默认情况下)任何子 Job 的非正常失败都会导致父 Job 及兄弟 Job 被取消。
底层机制:父子绑定过程
当协程启动时,内部通过 attachChild 建立联系。
源码视角的绑定
每当新协程启动,它会检查 coroutineContext[Job]。
- 若存在父 Job,新 Job 会调用
parentJob.attachChild(this)。 - 该方法返回一个
ChildHandle,并将子 Job 注册到父 Job 的私有状态中。 - 如果父 Job 已经处于取消中,子 Job 会立即收到取消指令。
结构化 vs. 非结构化对比
kotlin
val scope = CoroutineScope(Job() + Dispatchers.Default)
val job = scope.launch {
// 这个子协程与 scope 绑定
launch {
delay(1000)
println("子任务 1 完成")
}
launch {
delay(2000)
println("子任务 2 完成")
}
}
// 取消父 Job,所有内部协程立即停止
scope.cancel()kotlin
// ❌ 破坏结构化并发:GlobalScope 启动的协程是“孤儿”
GlobalScope.launch {
launch {
delay(1000)
println("我还在跑,即便启动我的外部环境可能已经销毁")
}
}
// 无法统一管理,极易导致内存泄漏和 CPU 浪费kotlin
val scope = CoroutineScope(Job())
scope.launch(Job()) { // ❌ 显式传入新 Job,切断了与 scope 的联系
delay(1000)
println("即使 scope.cancel(),我也不会停止")
}作用域构建器:coroutineScope
coroutineScope { ... } 是一个挂起函数,它会创建一个临时的作用域。它与 CoroutineScope 的区别在于:coroutineScope 会挂起当前协程,直到其内部所有子协程执行完毕。
适用场景
当你需要将一个大任务拆分为多个并发子任务,且必须等待它们全部完成后才能继续后续逻辑时,应使用 coroutineScope。
kotlin
suspend fun loadDashboardData() = coroutineScope {
val user = async { fetchUser() }
val news = async { fetchNews() }
// 等待 user 和 news 全部完成,且共享当前协程的结构化取消逻辑
Dashboard(user.await(), news.await())
}Android 中的工程实践
生命周期绑定
Android 开发中应始终优先使用官方提供的生命周期感知作用域,而非手动创建:
viewModelScope: 绑定到ViewModel,在onCleared()时自动取消。lifecycleScope: 绑定到LifecycleOwner(Activity/Fragment),在DESTROYED时取消。
业务组件中的自定义作用域
在 Repository 或 Service 等非 UI 组件中,推荐采用 AutoCloseable 或类似的清理模式:
kotlin
class NetworkMonitor(private val dispatcher: CoroutineDispatcher) : AutoCloseable {
// 使用 SupervisorJob 是为了防止单个监听任务失败导致整个监控器不可用
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
fun startMonitoring() {
scope.launch { /* ... */ }
}
override fun close() {
// 显式清理,确保资源不泄露
scope.cancel()
}
}核心准则总结
- 禁止使用
GlobalScope:除非是确实需要与应用生命周期一致的极少数后台任务。 - 不要在协程启动时传递新的
Job:这会破坏父子连接,除非你有意实现“启动即忘记(Fire and Forget)”且不关心其取消逻辑。 - 优先使用
coroutineScope构建器:它能自然地在挂起函数中引入结构化并发。