协程入门:协程概念、GlobalScope与runBlocking、launch与async、Job与Deferred
协程这东西,刚接触时容易觉得玄乎。说白了,它就是一套帮你管理异步任务的框架。我最早在Android项目里接触协程时,第一反应是:这不就是线程池换了个马甲吗?后来踩了几个坑才明白——协程的核心不是线程,而是挂起。
协程到底是什么?
协程是一种轻量级的并发方案。你可以把它理解成「可挂起的计算」。线程的切换由操作系统控制,而协程的挂起由代码自己控制。嗯,这个区别很关键。
举个例子。你在主线程里做网络请求,如果不小心,UI就会卡住。用线程吧,又得考虑线程池大小、回调嵌套等问题。协程的做法是:遇到耗时操作时,挂起当前协程,等结果回来再恢复执行。整个过程不阻塞线程。
核心要点:协程是运行在线程上的,但一个线程可以运行成千上万个协程。协程的挂起几乎不消耗资源。
GlobalScope 与 runBlocking
这两个东西,我建议你一开始就分清楚。它们代表了协程的两种启动方式。
GlobalScope
GlobalScope 是一个全局作用域。用它启动的协程,生命周期跟整个应用一样长。我在项目中见过有人用它来做定时任务,结果应用退出了协程还在跑——这其实是个坑。
GlobalScope.launch {
// 这个协程会一直运行,直到应用进程结束
delay(1000)
println("Hello from GlobalScope")
}
注意:GlobalScope 启动的协程无法被取消,除非进程结束。日常开发中尽量少用,除非你明确知道自己在做什么。
runBlocking
runBlocking 的作用是:阻塞当前线程,直到协程执行完毕。它通常用于main函数或测试代码中。我个人习惯只在写单元测试时用它,生产代码里几乎不用。
fun main() {
runBlocking {
delay(1000)
println("World")
}
println("Hello") // 这行会在协程执行完后才打印
}
为什么会这样?因为 runBlocking 会阻塞主线程,直到内部的协程完成。你想想看,如果生产代码里到处用 runBlocking,那跟直接写同步代码有什么区别?
launch 与 async
这两个是启动协程的两种方式。它们都返回一个对象,但用途不同。
launch
launch 启动一个协程,不返回结果。它返回一个 Job 对象,可以用来控制协程的生命周期。我经常用它来执行「只管做,不管结果」的任务,比如日志上传、数据预加载。
val job = CoroutineScope(Dispatchers.IO).launch {
// 执行耗时操作,不需要返回值
uploadLogs()
}
job.join() // 等待协程完成
async
async 启动一个协程,并返回一个 Deferred 对象。Deferred 是 Job 的子接口,多了一个 await() 方法,用来获取结果。说白了,async 就是带返回值的 launch。
val deferred = CoroutineScope(Dispatchers.Default).async {
// 执行计算并返回结果
computeResult()
}
val result = deferred.await() // 挂起直到拿到结果
我的建议:如果你需要并发执行多个任务并汇总结果,用 async 非常合适。比如同时请求三个接口,然后合并数据。
Job 与 Deferred
Job 代表一个协程的句柄。你可以用它来取消协程、等待协程完成、检查协程状态。Deferred 是 Job 的扩展,多了一个结果的概念。
| 特性 | Job | Deferred |
|---|---|---|
| 是否有返回值 | 无 | 有(通过 await()) |
| 取消 | 支持 cancel() | 支持 cancel() |
| 等待完成 | join() | await() |
| 典型使用场景 | 一次性任务、后台日志 | 并发计算、并行请求 |
我曾经在项目里犯过一个错:用 async 启动了一个任务,但忘了调用 await()。结果协程跑完了,结果却没人拿。嗯,这其实是个内存泄漏的隐患——Deferred 对象会一直持有结果引用。
知识体系图
下面这张图帮你理清协程入门阶段的核心概念和关系。
避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
- 不要在 GlobalScope 里做 UI 操作。GlobalScope 运行在后台线程,直接更新 UI 会崩溃。我曾经在 GlobalScope 里调了 setText(),结果闪退得莫名其妙。
- runBlocking 慎用。它阻塞当前线程,如果在主线程用 runBlocking,那跟 ANR 只差一步之遥。我建议只在测试代码和 main 函数里用。
- 记得取消协程。Activity 或 Fragment 销毁时,如果协程还在跑,轻则浪费资源,重则内存泄漏。用 viewModelScope 或 lifecycleScope 可以自动管理生命周期。
- async 别忘了 await()。不调用 await(),Deferred 的结果永远不会被消费。虽然不会报错,但算是一种资源浪费。
一句话总结:launch 只管做,async 要结果;GlobalScope 少用,runBlocking 慎用;Job 控制生命周期,Deferred 多一个结果。
协程入门其实不难,关键是理解挂起和恢复的机制。你想想看,以前用回调嵌套实现异步,代码像圣诞树一样一层套一层。协程用同步的写法做异步的事,代码可读性直接提升一个档次。嗯,这就是协程的魅力。
公众号:蓝海资料掘金营,微信deep3321