第1章 协程Scope基础概念
1.1 协程作用域定义
协程作用域(CoroutineScope)定义了协程的生命周期边界。每个协程都必须在某个作用域内启动,作用域负责管理其内部所有协程的取消与异常传播。在服务端编程中,典型的协程作用域对应一个请求、一个Session或一个后台任务的生命周期。
Kotlin标准库提供了三种内置作用域:
GlobalScope——全局生命周期,不推荐用于服务端,容易造成协程泄漏。coroutineScope——挂起函数,会等待内部所有协程完成,继承调用者上下文。supervisorScope——与coroutineScope类似,但一个子协程失败不会传播给兄弟协程。
关键概念: 在服务端代码中,永远不要直接使用
GlobalScope.launch。应通过自定义的 CoroutineScope 实例或结构化并发构建器来管理协程。1.2 结构化并发原则
结构化并发要求协程的生命周期与所在作用域的生命周期一致,形成树形层次结构。核心规则包括:
- 父子关系:在作用域内启动的子协程自动成为该作用域的子级。
- 取消传递:父作用域被取消时,所有子协程都会被递归取消。
- 异常传播:子协程的未捕获异常会向上传播至父作用域(
supervisorScope除外)。 - 等待完成:作用域必须等待所有子协程完成才能结束。
结构化并发避免了泄漏、资源未释放、孤立协程等问题,是构建可靠服务的基础。
| 构建器 | 异常传播 | 等待语义 | 典型场景 |
|---|---|---|---|
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)嵌套使用。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停止后仍然运行,造成资源泄漏和不可预测的状态。始终使用结构化并发来强制生命周期管理。