第3章 协程作用域与结构化并发

3.1 结构化并发核心概念

结构化并发是 Kotlin 协程的基石,它确保所有子协程的生命周期绑定到父作用域,避免协程泄漏。核心组件包括:

  • CoroutineScope —— 协程运行的上下文容器,管理子协程的生命周期。
  • Job —— 协程句柄,可追踪状态(New、Active、Completing、Completed、Cancelling、Cancelled),支持父子层级。
  • SupervisorJob —— 特殊 Job,子协程失败不会传播取消给兄弟或父协程。
  • 取消机制 —— 通过 job.cancel() 触发 CancellationException,协程内部可通过 isActive 或 suspend 函数检查取消。
  • onCancellation —— 在 finally 块中通过 isCancelled 判断,或使用 invokeOnCompletion 回调处理释放资源。
组件职责取消行为
CoroutineScope创建协程的入口,提供 Job 和 Dispatcher取消 scope 会取消所有子协程
Job控制单个协程生命周期取消 job 本身及其子 job
SupervisorJob允许子协程独立失败子协程失败不会取消兄弟或父协程
在 ViewModel 或 Fragment 中使用 viewModelScope 或 lifecycleScope,它们内部使用 SupervisorJob,确保单个数据请求失败不会影响其他请求。

3.2 结构化并发流程图

结构化并发父子 Job 关系与取消传播 CoroutineScope(root) 父 Job 子 Job A 子 Job B(失败) 兄弟 Job C 说明:在普通 Job 下,子 Job B 失败会导致父 Job 以及兄弟子 Job A 取消; 而在 SupervisorJob 下,子 Job B 失败不会传播取消到兄弟 Job C。

3.3 创建与取消协程作用域

两种常用方式:

// 方式1:使用 CoroutineScope 工厂
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch { 
    delay(1000); println("协程运行") 
}
scope.cancel() // 取消所有子协程

// 方式2:使用 SupervisorJob 允许独立失败
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
supervisorScope.launch { 
    throw RuntimeException("子任务A失败")
}
supervisorScope.launch { 
    delay(500); println("子任务B仍然执行") 
}
// 即使A失败,B仍正常完成
注意:在创建 scope 时,如果使用 SupervisorJob() ,必须将其作为参数传入,否则默认使用普通的 Job。SupervisorJob 默认不会影响父协程,但需要显式放在作用域中。

3.4 onCancellation 处理与资源清理

协程取消时会抛出 CancellationException,在 finally 块中通过 isCancelled 或 invokeOnCompletion 进行处理:

val job = scope.launch {
    try {
        repeat(100) { i ->
            delay(1000)
            println("处理第 $i 项")
        }
    } finally {
        // 不管正常结束还是取消都会执行
        if (isActive) {
            println("正常完成")
        } else {
            println("协程被取消,执行清理...")
            // 注意:在 finally 中调用 suspend 函数会触发取消异常,需用 withContext(NonCancellable)
            withContext(NonCancellable) {
                releaseResources()
            }
        }
    }
}
// 取消触发
delay(2500); job.cancel()

3.5 常见错误与调试

  • 错误1:未处理 CancellationException
    在 catch 中捕获 Exception 而不是 CancellationException,导致协程无法正常响应取消。应使用 catch (e: CancellationException) { throw e } 重新抛出。
  • 错误2:SupervisorJob 误用为最外层作用域
    使用 runBlocking(SupervisorJob()) 会导致 runBlocking 本身在子协程失败时立即结束,不符合预期。SupervisorJob 只适用于 launch/async 的子协程。
  • 错误3:在 finally 中调用 suspend 函数
    协程取消后 finally 块中调用 delay、withContext 等 suspend 函数会再次抛出 CancellationException,需用 withContext(NonCancellable) 包裹。
调试技巧:在协程的 invokeOnCompletion 回调中打印状态及异常信息,便于追踪取消链。使用 CoroutineExceptionHandler 全局捕获未处理的异常。

3.6 性能与稳定性优化建议

  1. 作用域复用:避免在每次请求时创建新的 CoroutineScope,应复用 Activity/Fragment 的生命周期作用域 (lifecycleScope, viewModelScope),减少创建开销。
  2. SupervisorJob 分离故障域:在 UI 层使用 SupervisorJob,确保单个网络请求失败不会导致所有协程取消;在后台数据同步层可以使用普通 Job 以保证原子性。
  3. 取消敏感协程使用 NonCancellable:对于必须完成的关键操作(如数据库写入),用 withContext(NonCancellable) 保证执行完成。
  4. 避免泄漏:在作用域取消时,子协程仍可能持有资源,务必在 finally 或 invokeOnCompletion 中释放网络连接、文件句柄等。