第1章 协程Scope基础概念

1.1 协程作用域定义

协程作用域(CoroutineScope)定义了协程的生命周期边界。每个协程都必须在某个作用域内启动,作用域负责管理其内部所有协程的取消与异常传播。在服务端编程中,典型的协程作用域对应一个请求、一个Session或一个后台任务的生命周期。

Kotlin标准库提供了三种内置作用域:

  • GlobalScope——全局生命周期,不推荐用于服务端,容易造成协程泄漏。
  • coroutineScope——挂起函数,会等待内部所有协程完成,继承调用者上下文。
  • supervisorScope——与coroutineScope类似,但一个子协程失败不会传播给兄弟协程。
关键概念: 在服务端代码中,永远不要直接使用 GlobalScope.launch。应通过自定义的 CoroutineScope 实例或结构化并发构建器来管理协程。

1.2 结构化并发原则

结构化并发要求协程的生命周期与所在作用域的生命周期一致,形成树形层次结构。核心规则包括:

  1. 父子关系:在作用域内启动的子协程自动成为该作用域的子级。
  2. 取消传递:父作用域被取消时,所有子协程都会被递归取消。
  3. 异常传播:子协程的未捕获异常会向上传播至父作用域(supervisorScope 除外)。
  4. 等待完成:作用域必须等待所有子协程完成才能结束。

结构化并发避免了泄漏、资源未释放、孤立协程等问题,是构建可靠服务的基础。

构建器异常传播等待语义典型场景
coroutineScope传播(子失败 → 父失败)等待所有子完成并行数据获取,失败即取消
supervisorScope不传播(子失败不影响其他)等待所有子完成独立任务的批处理,一个失败继续其他
withContext传播执行并返回结果切换调度器,子作用域单任务

1.3 Scope与CoroutineContext的关系

CoroutineScope 实际上是一个接口,核心成员只有 val coroutineContext: CoroutineContext。作用域通过其持有的 CoroutineContext 来继承调度器、Job、异常处理器和协程名称等元素。

关键等式:作用域 = 上下文 + 生命周期。上下文是静态配置,而作用域提供动态的协程管理能力。当你通过 CoroutineScope(context) 创建一个作用域时,该上下文中的 Job 会成为所有子协程的父Job。

// 服务端典型用法:为每个请求创建作用域
class UserService(private val externalScope: CoroutineScope) {
    fun fetchUser(id: String) {
        // 使用 externalScope 的子作用域,保证请求取消时自动终止
        externalScope.launch(Dispatchers.IO) {
            val user = apiClient.getUser(id)
            process(user)
        }
    }
}

// 结构化并发构建器
suspend fun fetchUserAndPosts(id: String): UserWithPosts = coroutineScope {
    val userDeferred = async { userRepo.getUser(id) }
    val postsDeferred = async { postRepo.getPostsByUser(id) }
    UserWithPosts(userDeferred.await(), postsDeferred.await())
}
性能提示: 避免为每个微任务创建独立的 CoroutineScope 实例。推荐在应用启动时创建有限数量的作用域(如按业务模块划分),并通过结构化并发构建器(coroutineScope / supervisorScope)嵌套使用。
协程Scope结构化并发关系 根 CoroutineScope 请求处理Scope 后台批处理Scope 协程: getUser 协程: getPosts 协程: sendEmail 子Scope继承父Job,取消根Scope → 所有子协程自动取消

1.4 调试技巧

启用JVM参数 -Dkotlinx.coroutines.debug 可以在日志中输出协程的ID和挂起点,便于跟踪scope内的协程活动。对于服务端,建议在应用启动时配置:

// 在 application.properties 或启动脚本中设置
-Dkotlinx.coroutines.debug=auto

同时,为协程设置有意义的名称可极大提高排查效率:

// 上下文命名
val requestScope = CoroutineScope(Job() + CoroutineName("request-${requestId}"))
requestScope.launch {
    // 日志中将显示当前协程名
}
常见错误: 在服务端滥用 GlobalScope.launch 或忘记存储作用域引用,导致协程在Service停止后仍然运行,造成资源泄漏和不可预测的状态。始终使用结构化并发来强制生命周期管理。