Job 和 Dispatcher 的不同组合方式,决定了 scope 是否具备结构化并发的取消能力
协程作用域就是 CoroutineScope,在它的内部你可以启动协程。创建一个 scope 很简单:
val scope = CoroutineScope()
但这样写会报错 —— 缺少必要的参数 context。它需要一个 CoroutineContext 类型的参数,即协程上下文,提供协程启动时需要的各种属性(如 Job 和 CoroutineDispatcher)。
// CoroutineScope 的构造函数签名
public fun CoroutineScope(
context: CoroutineContext
): CoroutineScope
CoroutineContext 是一个键值对集合,提供了协程运行时所需的各种配置。其中最重要的两个元素就是 Job(管理生命周期和取消)和 CoroutineDispatcher(决定协程运行在哪个线程)。
创建 scope 时可以只传 Job():
val scope = CoroutineScope(Job())
scope.launch {
// 在 Default 调度器上执行
}
上下文只有 Job() 而没有调度器,系统会自动补齐 Dispatchers.Default。这不是最佳实践,但确实是一个合格的 scope —— 你可以通过调用 scope.cancel() 来取消所有子协程。
只传 Job 的 scope 具备取消能力。因为 scope 的 context 中存在 Job,cancel() 扩展函数能找到它并级联取消所有子协程。
只传调度器也能启动协程:
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
// 在 IO 线程上执行
}
看起来能正常工作,但这里有一个关键陷阱:
当你调用 scope.launch 时,launch 会检查当前作用域的上下文中有没有 Job。如果没有,它会自动创建一个新的 Job,并成为新协程的父 Job。
但这个自动创建的 Job 是绑定到 launch 启动的那个协程的,而不是绑定到 scope 本身。也就是说:
scope.cancel() 来统一取消所有由它启动的协程cancel() 扩展函数无法工作// 只传调度器:cancel 无效!
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
delay(5000)
println("任务完成") // cancel 后依然会打印!
}
scope.cancel() // 无效!scope 内部没有 Job,取消不了任何东西
只传调度器虽然能启动协程,但破坏了结构化并发的取消能力。scope 变成了一个"漏勺",协程从它启动后就脱离管控了。
最规范的写法是两者都传:
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
// 在 Main 线程执行,受 scope 的 Job 管理
}
scope.cancel() // 有效!取消所有子协程
用 + 将多个 Context 元素组合在一起,这是 CoroutineContext 的设计特性 —— 它支持加法运算来合并多个元素。
CoroutineScope(Job()) —— 无调度器,默认 DefaultCoroutineScope(Dispatchers.IO) —— 无根 Job,无法 cancelCoroutineScope(Job() + Dispatchers.Main)lifecycleScope —— Android 已处理好一切viewModelScope —— ViewModel 专用在实际开发中,直接使用 lifecycleScope 或 viewModelScope 更方便,它们已经配置好了完整的上下文。手动创建 scope 一般只在特殊场景下才需要。
每个用 launch 启动的协程都有一个自己的 CoroutineScope,所以在协程中你可以直接启动另一个协程:
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch { // 父协程
// this 是 CoroutineScope
// 直接写 launch { } 等价于 this.launch { }
launch { // 子协程 A
apiService.fetchUser()
}
launch { // 子协程 B
apiService.fetchConfig()
}
}
这背后的原理在 五、协程取消与结构化并发 中详细讨论过 —— launch 的 block 参数类型是 suspend CoroutineScope.() -> Unit,大括号里有一个隐式的 CoroutineScope 作为 this。
外层 scope 管理着父协程,父协程自带 scope 管理着子协程。这就是结构化并发的层级关系 —— scope.cancel() 沿着这棵树一路向下,清理所有子孙协程。
CoroutineScope 的核心是 Job + Dispatcher 的组合 —— 缺了 Dispatcher 只是效率问题,缺了 Job 则彻底丧失结构化并发的取消能力。