CoroutineScope的创建与上下文配置

Job 和 Dispatcher 的不同组合方式,决定了 scope 是否具备结构化并发的取消能力

CoroutineScope CoroutineContext Job Dispatcher cancel 上下文

目录导航(点击跳转)

一、CoroutineScope 与 CoroutineContext 的关系

1CoroutineScope 是对 CoroutineContext 的包装

协程作用域就是 CoroutineScope,在它的内部你可以启动协程。创建一个 scope 很简单:

val scope = CoroutineScope()

但这样写会报错 —— 缺少必要的参数 context。它需要一个 CoroutineContext 类型的参数,即协程上下文,提供协程启动时需要的各种属性(如 JobCoroutineDispatcher)。

// CoroutineScope 的构造函数签名
public fun CoroutineScope(
    context: CoroutineContext
): CoroutineScope
CoroutineScope
包装
CoroutineContext
包含
Job
(取消管理)
CoroutineDispatcher
(线程调度)

CoroutineContext 是一个键值对集合,提供了协程运行时所需的各种配置。其中最重要的两个元素就是 Job(管理生命周期和取消)和 CoroutineDispatcher(决定协程运行在哪个线程)。

二、三种创建方式详解

2方式一:只传 Job

创建 scope 时可以只传 Job()

val scope = CoroutineScope(Job())

scope.launch {
    // 在 Default 调度器上执行
}

上下文只有 Job() 而没有调度器,系统会自动补齐 Dispatchers.Default。这不是最佳实践,但确实是一个合格的 scope —— 你可以通过调用 scope.cancel() 来取消所有子协程。

只传 Job 的 scope 具备取消能力。因为 scope 的 context 中存在 Job,cancel() 扩展函数能找到它并级联取消所有子协程。

3方式二:只传调度器 —— 有陷阱

只传调度器也能启动协程:

val scope = CoroutineScope(Dispatchers.IO)

scope.launch {
    // 在 IO 线程上执行
}

看起来能正常工作,但这里有一个关键陷阱

当你调用 scope.launch 时,launch 会检查当前作用域的上下文中有没有 Job。如果没有,它会自动创建一个新的 Job,并成为新协程的父 Job。

但这个自动创建的 Job 是绑定到 launch 启动的那个协程的,而不是绑定到 scope 本身。也就是说:

  • scope 对象内部实际上还是没有一个根 Job
  • 你无法通过 scope.cancel() 来统一取消所有由它启动的协程
  • 因为 scope 的上下文里没有 Job,cancel() 扩展函数无法工作
// 只传调度器:cancel 无效!
val scope = CoroutineScope(Dispatchers.IO)

scope.launch {
    delay(5000)
    println("任务完成")   // cancel 后依然会打印!
}

scope.cancel()  // 无效!scope 内部没有 Job,取消不了任何东西

只传调度器虽然能启动协程,但破坏了结构化并发的取消能力。scope 变成了一个"漏勺",协程从它启动后就脱离管控了。

4方式三:Job + Dispatcher —— 完整的 scope

最规范的写法是两者都传

val scope = CoroutineScope(Job() + Dispatchers.Main)

scope.launch {
    // 在 Main 线程执行,受 scope 的 Job 管理
}

scope.cancel()  // 有效!取消所有子协程

+ 将多个 Context 元素组合在一起,这是 CoroutineContext 的设计特性 —— 它支持加法运算来合并多个元素。

不推荐的写法

  • CoroutineScope(Job()) —— 无调度器,默认 Default
  • CoroutineScope(Dispatchers.IO) —— 无根 Job,无法 cancel

推荐的写法

  • CoroutineScope(Job() + Dispatchers.Main)
  • lifecycleScope —— Android 已处理好一切
  • viewModelScope —— ViewModel 专用

在实际开发中,直接使用 lifecycleScopeviewModelScope 更方便,它们已经配置好了完整的上下文。手动创建 scope 一般只在特殊场景下才需要。

三、每个协程都有自己的 Scope

5launch 创建的协程自带 CoroutineScope

每个用 launch 启动的协程都有一个自己的 CoroutineScope,所以在协程中你可以直接启动另一个协程:

val scope = CoroutineScope(Job() + Dispatchers.Main)

scope.launch {                        // 父协程
    // this 是 CoroutineScope
    // 直接写 launch { } 等价于 this.launch { }
    launch {                          // 子协程 A
        apiService.fetchUser()
    }
    launch {                          // 子协程 B
        apiService.fetchConfig()
    }
}

这背后的原理在 五、协程取消与结构化并发 中详细讨论过 —— launchblock 参数类型是 suspend CoroutineScope.() -> Unit,大括号里有一个隐式的 CoroutineScope 作为 this

  • scope (Job + Main)
    • 父协程 (自带 Scope)
      • 子协程 A
      • 子协程 B

外层 scope 管理着父协程,父协程自带 scope 管理着子协程。这就是结构化并发的层级关系 —— scope.cancel() 沿着这棵树一路向下,清理所有子孙协程。

四、核心思想总结

核心要点

一句话总结

CoroutineScope 的核心是 Job + Dispatcher 的组合 —— 缺了 Dispatcher 只是效率问题,缺了 Job 则彻底丧失结构化并发的取消能力。