1. 概述

在 Kotlin 协程开发中,CoroutineScope(协程作用域)和 CoroutineContext(协程上下文)是两个基础但容易混淆的概念。本文将深入剖析它们的本质区别、用途以及实际使用中的注意事项。

简单来说:
协程上下文 是一组与协程运行相关的数据集合,比如它在哪个线程执行、它的任务状态等。
协程作用域 则是用来启动和管理协程的“容器”,它持有一个上下文,并控制协程的生命周期。

理解这两者的分工,能帮你避免内存泄漏、任务失控等常见“踩坑”问题。


2. 协程作用域(Coroutine Scope)

要启动一个协程,必须通过 launchasync 这样的协程构建器(coroutine builder)。这些函数本质上是 CoroutineScope 接口的扩展函数。因此,任何协程都必须在某个作用域内启动

作用域的核心价值在于:

  • 建立协程之间的父子关系
  • 统一管理协程的生命周期(例如取消时自动清理所有子协程)

kotlinx.coroutines 提供了多个内置作用域,也支持自定义。

2.1. GlobalScope

最直接的方式是使用 GlobalScope

GlobalScope.launch {
    delay(500L)
    println("Coroutine launched from GlobalScope")
}

⚠️ 问题严重GlobalScope 的生命周期与整个应用绑定——但它不会阻止 JVM 退出。这意味着它像守护线程一样,可能在任务未完成时就被强制终止,极易造成资源泄露或任务丢失。

❌ 不推荐用于生产环境,尤其是需要保证任务完成的场景。


2.2. runBlocking

runBlocking 创建一个阻塞当前线程的作用域,直到其内部所有子协程执行完毕。

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Hello from nested coroutine")
    }
    println("Started")
    delay(2000L)
}

⚠️ 注意:它会阻塞线程,破坏了协程非阻塞的优势。
✅ 合适用途仅限于:

  • main 函数入口:确保程序等待协程结束
  • 测试代码中调用 suspend 函数

其他场景请避免使用,否则可能引发性能瓶颈。


2.3. coroutineScope

当你需要等待子协程完成,但又不想阻塞线程时,应该使用 coroutineScope

suspend fun doConcurrentWork() = coroutineScope {
    launch { 
        delay(1000L); println("Task 1 completed") 
    }
    launch { 
        delay(500L); println("Task 2 completed") 
    }
    println("All tasks started")
}

✅ 特点:

  • 是 suspend 函数,只挂起不阻塞
  • 等待所有子协程完成后再继续
  • 常用于并发任务编排(如并行请求合并结果)

📌 对比参考:runBlocking 与 coroutineScope 的区别


2.4. 自定义协程作用域

有时你需要更精细地控制协程行为,可以手动创建作用域:

val myScope = CoroutineScope(Dispatchers.Default + Job())

myScope.launch {
    println("Running in custom scope")
}

// 需要手动取消以释放资源
myScope.coroutineContext[Job]?.cancel()

✅ 使用场景:

  • Android 中配合 ViewModel 使用 viewModelScope
  • Service 层封装独立的业务作用域
  • 长生命周期组件中管理协程生命周期

⚠️ 记住:自定义作用域务必记得取消 Job,否则可能导致内存泄漏。


3. 协程上下文(Coroutine Context)

如果说作用域是“容器”,那上下文就是“配置”。它是协程运行所需的一组元素集合,类型为 CoroutineContext,本质是一个带索引的元素集,每个元素有唯一 key。

关键组成部分包括:

  • Job:控制协程的生命周期(启动、取消、等待)
  • Dispatcher:决定协程运行在哪个线程池

你可以用 + 操作符组合上下文元素:

launch(Dispatchers.Default + Job()) {
    println("Coroutine works in thread ${Thread.currentThread().name}")
}

3.1. 上下文中的 Job

每个协程都有一个关联的 Job,用于控制其执行状态。

可以通过以下方式访问:

val job = coroutineContext[Job]
println("Current job: $job")

常用操作:

  • job.join():等待协程完成
  • job.cancel():取消协程及其子协程
  • job.isActive, isCancelled:查询状态

3.2. 上下文与调度器(Dispatcher)

Dispatcher 决定了协程在哪类线程上运行。Kotlin 提供了几种常用实现:

调度器 用途
Dispatchers.Default CPU 密集型任务,默认线程数等于 CPU 核心数
Dispatchers.IO IO 密集型操作(文件读写、网络请求),共享线程池
Dispatchers.Main Android/iOS 主线程,用于更新 UI
Dispatchers.Unconfined 不限定线程,初始在调用者线程运行;挂起后恢复的线程由 suspend 函数决定

📌 小技巧:不要滥用 Unconfined,除非你清楚每次 resume 的线程来源。


3.3. 动态切换上下文:withContext

有时候我们需要在同一个协程中切换线程,比如先在 IO 线程读数据,再切回 Main 线程更新 UI。这时就要用到 withContext

newSingleThreadContext("Context 1").use { ctx1 ->
    newSingleThreadContext("Context 2").use { ctx2 ->
        runBlocking(ctx1) {
            println("Coroutine started in thread from ${Thread.currentThread().name}")
            withContext(ctx2) {
                println("Coroutine works in thread from ${Thread.currentThread().name}")
            }
            println("Coroutine switched back to thread from ${Thread.currentThread().name}")
        }
    }
}

withContext 的优势:

  • 挂起而非阻塞
  • 自动合并上下文(保留原有 Job,替换 Dispatcher)
  • 返回值可用于链式处理

📌 典型用例:

val result = withContext(Dispatchers.IO) { fetchDataFromNetwork() }
withContext(Dispatchers.Main) { updateUi(result) }

3.4. 协程的父子关系

当你在一个协程内部启动另一个协程,默认会继承父协程的上下文,且新协程的 Job 成为父 Job 的子 Job:

launch {
    val child = launch {
        delay(1000L)
        println("Child executed")
    }
    child.join()
}

这种结构的好处是:

  • 父协程取消时,所有子协程自动级联取消
  • 结构化并发得以保障

但如果你希望协程独立运行(不受父协程影响),有两种方式打破父子关系:

  1. 指定不同的作用域

    GlobalScope.launch { ... }
    
  2. 传入独立的 Job

    launch(coroutineContext + Job()) { ... }
    

✅ 应用场景举例:

  • 启动后台心跳任务,即使页面销毁也不中断
  • 日志上报等“火后即忘”型任务

⚠️ 注意:脱离作用域的协程需自行管理生命周期,防止泄漏。


4. 总结

对比项 协程作用域(Scope) 协程上下文(Context)
定义 启动和管理协程的容器 协程运行所需的配置集合
核心职责 控制生命周期、结构化并发 指定运行线程、任务状态
关键接口 CoroutineScope CoroutineContext
是否可组合 ❌ 不能叠加 ✅ 支持 + 操作符合并
常见误用 GlobalScope 导致泄漏 错用 Unconfined 引发线程混乱

📌 核心要点回顾:

  • ✅ 所有协程必须在作用域中启动
  • ✅ 优先使用结构化并发(如 viewModelScope, lifecycleScope
  • ✅ 用 withContext 切换线程,而不是手动调度
  • ✅ 明确父子关系,避免意外取消或泄漏

所有示例代码均可在 GitHub 获取:https://github.com/baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-concurrency


原始标题:Difference Between Coroutine Scope and Coroutine Context

« 上一篇: Kotlin 多维数组详解
» 下一篇: 解析 URL 字符串