第一章:协程初探——从“轻量级线程”说起
说实话,我第一次接触协程这个概念时,脑子里冒出的第一个念头就是:“这不就是线程的包装吗?”
后来踩了不少坑,才真正理解——协程和线程,压根儿就不是一个层面的东西。今天咱们就从最基础的地方聊起,把协程的来龙去脉理清楚。
1.1 什么是协程?
协程,英文叫 Coroutine。拆开来看,“Co”是协作,“Routine”是例程。合起来就是“协作式例程”。
什么意思呢?
你想想看,线程是由操作系统调度的,它什么时候执行、什么时候暂停,你控制不了。但协程不一样——协程的切换是程序自己控制的,说白了就是“我主动让出 CPU,而不是被操作系统抢走”。
举个例子:
// 这是一个简单的协程
fun main() = runBlocking {
launch {
println("协程开始工作")
delay(1000) // 挂起,但不阻塞线程
println("协程恢复执行")
}
println("主线程继续做其他事")
}
你看,delay 和 Thread.sleep 最大的区别是什么?delay 只是挂起了协程,线程本身并没有被阻塞。这意味着,同一个线程可以同时运行成千上万个协程。
核心理解:协程是运行在线程上的“轻量级任务”。一个线程可以承载成千上万个协程,而协程的切换几乎零开销。
1.2 协程与线程的区别
我记得刚带团队时,有个同事问我:“既然线程也能做异步,为什么还要用协程?”
这个问题问得好。咱们直接上对比:
| 对比维度 | 线程 | 协程 |
|---|---|---|
| 调度方式 | 操作系统抢占式调度 | 程序自身协作式调度 |
| 创建开销 | 较大(约 1MB 栈空间) | 极小(约几 KB) |
| 切换成本 | 涉及内核态切换,成本高 | 用户态切换,几乎零成本 |
| 并发数量 | 几百个就吃力 | 轻松上万 |
| 阻塞行为 | 阻塞线程 | 挂起协程,不阻塞线程 |
说白了,线程是“重量级选手”,协程是“轻量级选手”。
我在项目中遇到过这样一个场景:一个直播 App 需要同时拉取 500 个房间的在线人数。如果用线程,光创建线程的开销就能把手机搞卡。换成协程后,一个线程就搞定了所有请求,流畅度提升非常明显。
个人习惯:我一般这样区分——如果你需要并行计算(利用多核 CPU),用线程;如果你需要高并发 I/O(网络请求、数据库读写),用协程。
1.3 Kotlin 协程的核心概念
好,现在咱们来聊聊 Kotlin 协程的三个核心概念:Job、CoroutineScope、CoroutineContext。
这三个东西,你刚开始可能会觉得抽象。别急,咱们一个一个拆。
1.3.1 Job —— 协程的“身份证”
Job 代表一个协程任务。你可以用它来:
- 检查协程是否完成
- 取消协程
- 等待协程执行完毕
val job = launch {
repeat(100) { i ->
println("任务执行中: $i")
delay(500)
}
}
// 5 秒后取消任务
delay(5000)
job.cancel() // 取消协程
job.join() // 等待协程真正结束
这里有个坑,我曾经踩过:cancel() 只是发出取消信号,协程并不会立刻停止。它需要在下一次挂起点(比如 delay、withContext)时才会响应取消。
避坑指南:我曾经在项目里直接调了 cancel() 就以为万事大吉,结果协程里的资源一直没释放。后来才发现,必须配合 join() 或者使用 cancelAndJoin() 才能确保协程完全结束。
1.3.2 CoroutineScope —— 协程的“容器”
CoroutineScope 定义了协程的“生命周期”。
你想想看,每个协程都需要一个“老板”来管理它。这个老板就是 CoroutineScope。当老板被销毁时,它手下的所有协程都会被自动取消。
class MyActivity : AppCompatActivity() {
// 创建一个与 Activity 生命周期绑定的作用域
private val scope = MainScope()
fun loadData() {
scope.launch {
// 在这里启动协程
val data = fetchData()
updateUI(data)
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel() // 销毁所有协程,防止内存泄漏
}
}
我个人习惯在 Android 项目里用 lifecycleScope 或 viewModelScope,它们已经帮我们绑定了生命周期,省心不少。
1.3.3 CoroutineContext —— 协程的“配置清单”
CoroutineContext 是协程的上下文,它包含了协程运行所需的各种配置信息。说白了,就是告诉协程“在哪个线程跑”、“出错了怎么办”、“叫什么名字”等等。
它主要由以下几个元素组成:
- Dispatcher:指定协程在哪个线程上执行
- Job:协程任务本身
- CoroutineName:给协程起个名字,方便调试
- CoroutineExceptionHandler:异常处理器
// 创建一个自定义的上下文
val customContext = Dispatchers.IO +
CoroutineName("MyCoroutine") +
CoroutineExceptionHandler { _, e ->
println("出错了: ${e.message}")
}
scope.launch(customContext) {
// 这个协程在 IO 线程执行,名字叫 MyCoroutine
// 如果抛出异常,会被上面的处理器捕获
}
这里有个细节:+ 运算符用于组合多个上下文元素。如果出现冲突(比如两个 Dispatcher),右边的会覆盖左边的。
小技巧:调试协程问题时,我习惯给每个协程加上 CoroutineName。这样在日志里一眼就能看出是哪个协程出了问题,排查效率高很多。
1.4 三者之间的关系
咱们用一张图来理解:
- CoroutineScope 是“老板”,管理协程的生命周期
- Job 是“员工”,代表具体的协程任务
- CoroutineContext 是“工作手册”,告诉员工怎么干活
当你调用 scope.launch { ... } 时,实际上发生了三件事:
- 创建一个新的 Job
- 将 Job 与 scope 的上下文合并
- 在新上下文中启动协程
嗯,到这里,协程的三大核心概念你应该有个基本认识了。下一章咱们会深入聊 launch 和 async 的区别,以及如何在实际项目中用好它们。
记住一句话:协程不是银弹,但用好了,它能让你的异步代码像同步代码一样优雅。