Skip to content

结构化并发

源:Structured concurrency

结构化并发(Structured Concurrency)是 Kotlin 协程的设计基石。它不仅仅是一种父子层级关系,更是一种确保协程任务生命周期确定性资源不泄露以及错误可传播的编程范式。

核心定义与 API 契约

在结构化并发中,新的协程只能在特定的 CoroutineScope 中启动。该作用域划定了协程的生命周期范围,确保所有子协程在父作用域结束前必须完成。

CoroutineScope 签名

查看 CoroutineScope 定义
kotlin
public interface CoroutineScope {
    /**
     * 该作用域的上下文。
     * 结构化并发要求上下文必须包含一个 Job 用于管理生命周期。
     */
    public val coroutineContext: CoroutineContext
}

Job 的分层契约

Job 是结构化并发的物理句柄。当你在一个 CoroutineScope 中调用 launchasync 时,新协程的 Job 会自动成为父作用域 Job 的子项,形成一棵 Job 树

  • 确定性完成:父 Job 只有在所有子 Job 均进入 CompletedCancelled 状态后,才能结束自己的 Completing 状态。
  • 取消传播:父 Job 被取消时,会递归地取消其所有子 Job。
  • 异常连坐:(默认情况下)任何子 Job 的非正常失败都会导致父 Job 及兄弟 Job 被取消。

底层机制:父子绑定过程

当协程启动时,内部通过 attachChild 建立联系。

源码视角的绑定

每当新协程启动,它会检查 coroutineContext[Job]

  1. 若存在父 Job,新 Job 会调用 parentJob.attachChild(this)
  2. 该方法返回一个 ChildHandle,并将子 Job 注册到父 Job 的私有状态中。
  3. 如果父 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()
    }
}

核心准则总结

  1. 禁止使用 GlobalScope:除非是确实需要与应用生命周期一致的极少数后台任务。
  2. 不要在协程启动时传递新的 Job:这会破坏父子连接,除非你有意实现“启动即忘记(Fire and Forget)”且不关心其取消逻辑。
  3. 优先使用 coroutineScope 构建器:它能自然地在挂起函数中引入结构化并发。