17、协程基础:协程的概念、GlobalScope与runBlocking、launch与async、挂起函数
协程,这个词你肯定听过。说白了,它就是 Kotlin 用来处理异步任务的轻量级方案。我刚开始接触时,总觉得它跟线程差不多,后来踩了坑才明白——完全不是一回事。
线程是操作系统调度的,创建和切换成本都不低。协程呢?它跑在线程之上,由编译器帮你管理状态。你可以理解成:协程是「可控的、可挂起的计算片段」。挂起时不阻塞线程,恢复时接着往下走。嗯,这个特性非常关键。
核心一句话:协程是轻量级线程,但比线程更灵活、更省资源。
17.1 协程的基本概念
协程本质上是一个状态机。你写出来的看似顺序的代码,编译器会帮你拆成多个状态块。遇到挂起函数时,协程就暂停执行,保存当前状态;等条件满足再恢复。
我个人习惯把协程想象成「可以暂停的函数」。普通函数一旦调用,必须从头跑到尾。协程不一样,它可以在中间停下来,去做别的事,之后再回来继续跑。
举个例子:
suspend fun fetchUserData(): String {
// 模拟网络请求
delay(1000) // 挂起,不阻塞线程
return "用户数据"
}
这里的 delay 就是挂起函数。它不会让当前线程睡过去,而是把协程挂起,线程可以去干别的活。一秒钟后协程恢复,返回数据。这个机制,就是协程最核心的魅力。
我的经验:在 Android 项目里,我经常用协程替代回调。代码从「回调地狱」变成了顺序书写,可读性提升一大截。
17.2 GlobalScope 与 runBlocking
这两个东西,是入门时最容易混淆的。我刚开始学的时候也搞反过,后来才理清楚。
GlobalScope
GlobalScope 是一个全局作用域。用它启动的协程,生命周期跟整个应用一样长。听起来很爽对吧?但我要提醒你:慎用。
我曾经在一个后台服务里用了 GlobalScope.launch 去拉数据,结果 Activity 都销毁了,协程还在跑。内存泄漏、空指针,一个接一个来。从那以后,我只有在极少数场景(比如独立的后台任务)才用它。
GlobalScope.launch {
// 这个协程会一直跑,直到应用退出
delay(5000)
println("5秒后执行")
}
runBlocking
runBlocking 正好相反。它会阻塞当前线程,直到协程执行完毕。说白了,它是用来「桥接」普通代码和协程世界的。
你想想看,在 main 函数里想测试协程,怎么办?用 runBlocking 包一层就行。
fun main() {
runBlocking {
delay(1000)
println("Hello, 协程!")
}
// 这里会等上面的协程执行完才打印
println("main 结束")
}
注意:runBlocking 会阻塞线程,不要在 Android 主线程里用它。否则 UI 卡死,用户会骂娘的。我见过有人这么干,结果 ANR 弹个不停。
| 特性 | GlobalScope | runBlocking |
|---|---|---|
| 是否阻塞线程 | 否 | 是 |
| 生命周期 | 应用级别 | 代码块级别 |
| 典型用途 | 后台常驻任务 | 测试、main函数 |
| 风险 | 内存泄漏 | 线程阻塞 |
17.3 launch 与 async
这两个是启动协程的「两把刷子」。它们都用来创建协程,但返回值不同,用途也不同。
launch:只管点火,不管结果
launch 返回一个 Job 对象。它启动协程后,不关心返回值。适合「发完就忘」的场景,比如日志上报、UI 刷新。
val job = GlobalScope.launch {
// 模拟耗时操作
delay(2000)
println("任务完成")
}
// job 可以取消
job.cancel()
我在项目里常用 launch 来做一些「非关键」的异步操作。比如用户点击按钮后,后台记录一下点击事件,不需要等结果。
async:要结果的,用这个
async 返回 Deferred 对象,它继承自 Job,但多了一个 await() 方法。调用 await() 会挂起协程,直到拿到结果。
val deferred = GlobalScope.async {
delay(1000)
"异步结果"
}
runBlocking {
val result = deferred.await() // 挂起等待
println(result) // 输出:异步结果
}
你想想看,如果同时发起两个网络请求,各自返回结果后再合并,用 async 就非常优雅:
runBlocking {
val data1 = async { fetchDataFromApi1() }
val data2 = async { fetchDataFromApi2() }
val combined = data1.await() + data2.await()
println(combined)
}
总结:launch 是「射箭」,射出去就不管了;async 是「钓鱼」,你得等着收竿。
17.4 挂起函数
挂起函数是协程的「灵魂」。用 suspend 关键字修饰的函数,可以在不阻塞线程的情况下暂停执行。
但要注意:挂起函数只能在协程内部或其他挂起函数中调用。你不能在普通函数里直接调用它。
// 这是一个挂起函数
suspend fun doHeavyWork(): Int {
delay(3000) // 模拟耗时
return 42
}
// 普通函数里不能直接调用
fun normalFunction() {
// doHeavyWork() // 编译错误!
}
// 必须在协程里调用
fun main() = runBlocking {
val result = doHeavyWork()
println(result)
}
挂起函数到底做了什么?我简单解释一下:
- 遇到
delay、await等挂起点时,协程会暂停 - 当前线程可以去做别的事
- 挂起点结束后,协程恢复执行
- 整个过程,线程没有被阻塞
我曾经在面试时被问到:「挂起函数和普通函数有什么区别?」我的回答是:普通函数是「一条路走到黑」,挂起函数是「走到一半可以停下来喝口水,喝完继续走」。
避坑指南:挂起函数里不要做太耗 CPU 的计算。挂起是为了等待 I/O,不是让你做死循环。如果真有大量计算,用 withContext(Dispatchers.Default) 切到后台线程。
17.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以对照着回顾一下:
这张图里,我把协程作用域、构建器、挂起函数三大块串起来了。你从中心出发,沿着箭头走一遍,基本就能理清本章的知识脉络。
好了,协程基础就讲到这里。记住:多写、多试、多踩坑,才能真正理解协程的妙处。我当年也是从「这什么鬼」到「真香」一步步过来的。