协程上下文:CoroutineContext、调度器、作用域与结构化并发
协程上下文,说白了就是协程运行时的「环境配置包」。我刚开始学 Kotlin 协程时,总觉得这玩意儿有点抽象。后来在项目里踩了几次坑,才真正理解它有多重要。
今天咱们就把 CoroutineContext、Dispatchers、协程作用域和结构化并发这几个概念串起来讲。你会发现,它们其实是一套完整的设计哲学。
一、CoroutineContext:协程的「身份证」
每个协程都携带一个 CoroutineContext。它像是一个不可变的 Map,里面存着协程运行所需的各种元素。常见的元素有:
- Job:控制协程的生命周期
- CoroutineDispatcher:决定协程跑在哪个线程
- CoroutineName:给协程起个名字,方便调试
- CoroutineExceptionHandler:处理未捕获的异常
我个人习惯用 + 运算符来组合这些元素,就像搭积木一样:
val context = Job() + Dispatchers.IO + CoroutineName("MyCoroutine")
launch(context) {
// 协程体
}
这里要注意,+ 右边的元素会覆盖左边的同名元素。比如你写了两个 Dispatchers,后面的会生效。
二、Dispatchers:协程的「交通工具」
调度器决定了协程跑在哪个线程池上。Kotlin 内置了四种调度器:
| 调度器 | 用途 | 线程数 |
|---|---|---|
| Dispatchers.Main | UI 操作(Android、Swing 等) | 1(主线程) |
| Dispatchers.IO | 网络请求、文件读写 | 64 或更多 |
| Dispatchers.Default | CPU 密集型计算 | 等于 CPU 核心数 |
| Dispatchers.Unconfined | 不指定线程(极少用) | 调用者线程 |
嗯,这里有个坑。我曾经在项目里用 Dispatchers.Default 做网络请求,结果发现并发量上不去。后来才意识到,Default 的线程数受 CPU 核心数限制,而 IO 操作应该用 Dispatchers.IO,它的线程池更大。
// 错误示范:用 Default 做网络请求
launch(Dispatchers.Default) {
val result = api.fetchData() // 阻塞等待,浪费 CPU 线程
}
// 正确做法:用 IO 调度器
launch(Dispatchers.IO) {
val result = api.fetchData() // 合理利用 IO 线程池
}
withContext(Dispatchers.IO) 更新 UI,结果直接崩溃。记住:UI 操作必须切回 Main 调度器。
三、协程作用域:协程的「活动范围」
协程不能凭空启动,它必须在一个作用域内。作用域定义了协程的生命周期边界。常见的几个作用域:
- GlobalScope:全局作用域,进程级别。不推荐日常使用,容易造成泄漏。
- lifecycleScope:Android 中绑定 Activity/Fragment 生命周期。
- viewModelScope:Android 中绑定 ViewModel 生命周期。
- 自定义 CoroutineScope:最灵活的方式。
为什么要有作用域?你想想看,如果每个协程都像野马一样不受控制,那内存泄漏、任务泄漏就是家常便饭。作用域就是给协程套上缰绳。
class MyViewModel : ViewModel() {
// 自定义作用域,绑定到 ViewModel 生命周期
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun loadData() {
scope.launch {
// 协程体
}
}
override fun onCleared() {
super.onCleared()
scope.cancel() // 页面销毁时取消所有协程
}
}
四、结构化并发:协程的「组织纪律」
结构化并发是 Kotlin 协程最优雅的设计之一。它的核心思想是:父协程管理子协程的生命周期。
举个例子:
scope.launch { // 父协程
launch { // 子协程 1
delay(1000)
println("子协程 1 完成")
}
launch { // 子协程 2
delay(2000)
println("子协程 2 完成")
}
println("父协程等待子协程...")
}
这里有个关键点:父协程会等待所有子协程完成,然后自己才结束。如果父协程被取消,所有子协程也会被递归取消。这就是结构化并发的威力——一个取消,全部取消。
我记得有一次在重构代码时,发现某个页面退出后后台还在跑网络请求。排查了半天,发现是用了 GlobalScope.launch。改成结构化并发后,问题迎刃而解。
coroutineScope 或 supervisorScope 来创建作用域。前者在子协程失败时会取消所有兄弟协程,后者则不会。根据业务场景选择合适的策略。
五、知识体系总览
下面这张图总结了本章的核心概念和它们之间的关系:
从图中可以看出,CoroutineContext 是协程的配置中心,它包含 Job、Dispatcher 等元素。而 CoroutineScope 则负责管理协程的生命周期,结构化并发则是作用域的具体实现机制。
六、实战中的选择策略
在实际项目中,我一般遵循这几个原则:
- UI 层:使用 Android 自带的 lifecycleScope 或 viewModelScope,自动绑定生命周期。
- 业务层:自定义 CoroutineScope,传入 SupervisorJob + Dispatchers.IO,手动管理取消。
- 工具类:使用
coroutineScope { }创建临时作用域,确保任务完成后自动清理。 - 全局任务:尽量避免。如果确实需要(比如日志上报),用 GlobalScope 但要格外小心。
好了,关于协程上下文的核心内容就讲到这里。记住,理解这些概念最好的方式就是动手写代码。下次遇到协程泄漏或线程问题,不妨先从这几个角度排查一下。