协程入门:协程概念、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 对象会一直持有结果引用。

知识体系图

下面这张图帮你理清协程入门阶段的核心概念和关系。

协程 Coroutine 启动方式 GlobalScope runBlocking CoroutineScope 协程构建器 launch → Job async → Deferred Job: 控制协程生命周期 | Deferred: Job + 结果

避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路。

  • 不要在 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