CoroutineScope — 它是什么,生存范围和在协程中的工作原理

作者: IT Sectr 发布日期: 2026-06-22 阅读时间: 10 分钟

CoroutineScope — 是 Kotlin 接口,它定义了协程的生存范围并为启动新协程提供上下文。根据 Kotlin 文档,2025,每个 CoroutineScope 实例都包含 CoroutineContext 并管理其中启动的所有协程。当 scope 结束时(cancel),所有子协程都会自动取消,这可以防止内存泄漏。

主要内容

  • CoroutineScope — 具有单一 CoroutineContext 字段的接口,定义协程的生命周期
  • Job — 负责取消的上下文元素:取消 scope 会取消所有子协程
  • 结构化竞争 — 子协程绑定到父 scope 的原则
  • GlobalScope — 整个应用程序的 scope,由于泄漏风险不建议使用
  • supervisorScope — 特殊 scope,其中取消一个子协程不会取消其他协程

Kotlin 中的 CoroutineScope 是什么?

CoroutineScope — 是来自 kotlinx.coroutines 库的基本接口,作为协程的容器。它定义了协程的生存边界:当 scope 结束时,其中的所有协程都会自动取消。

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

该接口仅包含一个字段 — coroutineContext。通过它,scope 为其中启动的所有协程提供调度器(Dispatcher)、任务(Job)、异常处理和其他上下文元素。

在 kotlinx.coroutines 库中的作用

所有协程启动函数 — launchasyncrunBlocking — 都是 CoroutineScope 的扩展函数。这意味着只有在存在 scope 对象时才能调用它们。这种设计保证了每个协程都有明确定义的父级和生命周期。

CoroutineScope 的应用场景

在 Android 中,每个架构组件都有自己的 scope:ViewModel 的 viewModelScope,Activity/Fragment 的 lifecycleScope。在服务器应用程序中,scope 可以绑定到 HTTP 请求或数据库连接池。

CoroutineScope 如何工作:Job 和结构化竞争

理解 CoroutineScope 的内部结构需要熟悉 Job 的概念和 结构化竞争 的原则。

Job — 协程的任务

每个协程在启动时都会返回一个 Job 对象(或 async 的 Deferred)。Job 代表一个具有明确生命周期的任务:New、Active、Completing、Completed、Cancelling、Cancelled。Job 对象形成树状结构:

  • 父 Job — 协程启动所在的 scope
  • 子 Job — 通过 launch/async 启动的每个协程
  • 取消父级 → 取消所有子级
  • 子级中的异常 → 取消父级(除非 supervisorScope)

结构化竞争原则

结构化竞争 — Kotlin Coroutines 的关键架构原则,其中协程的寿命与其 scope 的寿命相绑定。这与“fire-and-forget”模型形成对比,在该模型中协程在 scope 结束后继续存活。结构化竞争的优点:

  • 可预测的生命周期 — 当 scope 结束时,所有协程都保证停止
  • 自动错误处理 — 任何子协程中的异常都会传播到 scope
  • 无泄漏 — scope 结束后没有任何协程保持活跃
  • 清晰的层级结构 — 代码反映了并行操作的逻辑结构

CoroutineScope 的生命周期

当调用 scope.cancel() 时,Job scope 进入 Cancelled 状态,这会递归地取消所有子 Job。取消后,scope 只能通过创建新的 CoroutineScope 实例来重新使用。

创建和配置 CoroutineScope

CoroutineScope 可以通过工厂函数或通过在类中实现接口来创建。我们来看看这两种方法。

工厂函数 CoroutineScope()

kotlin
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

scope.launch {
    println("在 ${Thread.currentThread().name} 上运行")
}

工厂函数接受 CoroutineContext 并使用指定的上下文创建 scope。在示例中,CPU 密集型任务使用 Dispatchers.Default,并使用 SupervisorJob 来隔离子协程之间的异常。

通过组合实现接口

kotlin
class MyRepository {
    private val scope = CoroutineScope(Dispatchers.IO + Job())

    suspend fun fetchData(): Data = scope.async {
        api.getData()
    }.await()

    fun cleanup() {
        scope.cancel()
    }
}

我们将 scope 作为类的字段存储,并手动调用 cleanup 来取消它。这适合于具有可管理生命周期的组件 — 例如,仓库或管理器。

通过委托实现

Kotlin 允许通过关键字 by 委托 CoroutineScope 的实现:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // 协程在 DataLoader scope 中运行
        }
    }
}

这种方法在类本身就是 scope 并希望提供协程启动方法时很方便。但是请谨慎:类会继承 CoroutineScope 的所有方法,包括 cancel,这可能会破坏封装性。

GlobalScope 与自定义 CoroutineScope

GlobalScope — 是整个应用程序的 CoroutineScope 单例。官方不建议在生产代码中使用它。

GlobalScope 的问题

  • 缺乏结构化竞争 — GlobalScope 中的协程不绑定到组件的生命周期
  • 内存泄漏 — 协程可能在 Activity/Fragment 关闭后继续运行
  • 测试困难 — GlobalScope 在测试中无法替换
  • 资源消耗不可控 — 许多协程可能比预期运行更长时间

GlobalScope 何时合理

JetBrains 仅允许在稀有场景下使用 GlobalScope:应用程序级别的后台进程,这些进程在所有 Activity 关闭后必须保持活跃(例如,数据同步、分析)。但即使在这些情况下,更好的做法是使用 CoroutineScope(SupervisorJob()) 创建自己的 scope。

建议

始终使用具有明确生命周期管理的 自定义 CoroutineScope。在 Android 中,这些是 viewModelScope 和 lifecycleScope。在服务器应用程序中,为每个请求或连接池创建 scope。

coroutineScope 与 supervisorScope:有什么区别

这两个函数都是暂停函数,用于为并行任务创建临时 scope,但它们在异常时的行为根本不同。

特征coroutineScopesupervisorScope
错误时的行为子协程中的异常会取消所有其他协程子协程中的异常不会取消其他协程
错误传播是,第一个异常会传播到外部是,第一个异常会传播到外部
默认 JobJob() — 子级绑定到父级SupervisorJob() — 子级之间相互独立
典型用例由多个步骤组成的原子操作独立的并行任务(UI 加载)

何时选择 coroutineScope

当多个并行操作构成一个原子操作时,使用 coroutineScope。例如,从三个服务器加载数据:如果一个请求失败,其他请求就没有意义了。

kotlin
suspend fun loadProductPage(): ProductPage = coroutineScope {
    val product = async { api.getProduct() }
    val reviews = async { api.getReviews() }
    ProductPage(product.await(), reviews.await())
}

如果 getProduct 或 getReviews 抛出异常 — 两个协程都会被取消,并且异常会传播到调用代码。

何时选择 supervisorScope

当并行操作之间不相互依赖时,使用 supervisorScope。例如,在多个独立区域中加载个人资料数据:如果推荐区域失败,个人资料标题和好友列表应该正常显示。

使用 CoroutineScope 时的常见错误

我们来看看开发者在 Kotlin 中使用 CoroutineScope 时最常见的错误。

错误 1:忘记取消 scope

最常见的协程泄漏场景 — 在组件结束时创建 scope 而未调用 cancel。如果 scope 未被取消,协程会继续运行,保持对对象的引用。在 Android 中,使用会自动取消的 viewModelScopelifecycleScope

错误 2:在 Activity 或 Fragment 中使用 GlobalScope

GlobalScope 忽略 Android 组件的生命周期。在 Activity 关闭后,在 GlobalScope 中启动的协程会继续运行并尝试更新 UI — 这将导致崩溃。UI 组件始终应使用 lifecycleScope。

错误 3:重新使用已取消的 scope

调用 cancel() 后,scope 不能重新使用 — 其中的所有协程已经结束。通过工厂函数创建新的 CoroutineScope 实例。Job() 不支持重新激活。

错误 4:CoroutineScope 接口的错误委托

通过 by 委托时,类会获得可以从任何地方调用的公共 cancel() 方法,这会破坏封装性。将 scope 作为私有字段存储,不要委托接口。

常见问题

CoroutineScope 和 CoroutineContext 有什么区别?

CoroutineScope — 是拥有 CoroutineContext 并负责协程生命周期的接口。CoroutineContext — 是一组元素(调度器、job、错误处理),定义了协程“如何”执行。其中一个区别是:scope 创建协程,context 管理它们的行为。

可以用 SupervisorJob 创建 CoroutineScope 吗?

可以,这是标准模式:CoroutineScope(Dispatchers.IO + SupervisorJob())。SupervisorJob 防止在一个子协程中出现异常时流式取消其他子协程。这对于独立的并行任务很有用,其中一个错误不应该停止其他任务。

CoroutineScope 可以容纳多少个协程?

scope 中的协程数量没有限制 — 只受到可用内存和调度器设置的限制。实际限制通常是一个 scope 中的 数千个活跃协程。但是,大量的协程可能表明架构问题。

如何测试带有 CoroutineScope 的代码?

正确的方法是通过构造函数将 scope 传递给类,或使用来自 kotlinx-coroutines-test 的 runBlockingTest / runTest。在测试中,可以将 scope 替换为 TestCoroutineDispatcher 并手动控制协程的执行。

一个协程可以拥有自己的 scope 吗?

不可以,scope 是协程的外部容器。协程本身不是 scope。但是,在协程内部,可以通过 coroutineScopesupervisorScope 创建新 scope 来并行启动子协程。

总结

  • CoroutineScope — 具有 coroutineContext 字段的接口,定义其中启动的协程的生命周期
  • 结构化竞争 — 取消 scope 会自动取消所有子协程,防止内存泄漏
  • Job 和 SupervisorJob — 两种错误处理模式:流式取消(Job)和隔离错误(SupervisorJob)
  • GlobalScope — 由于缺乏与生命周期的绑定,不建议用于生产环境
  • coroutineScope 与 supervisorScope — 原子并行操作与独立并行任务
  • viewModelScope 和 lifecycleScope — 为 Android 提供的现成 scope,在组件结束时自动取消
  • 工厂函数 — 通过 CoroutineContext + 显式调用 cancel 创建 scope 的首选方法

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读