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)
}

挂起函数到底做了什么?我简单解释一下:

  • 遇到 delayawait 等挂起点时,协程会暂停
  • 当前线程可以去做别的事
  • 挂起点结束后,协程恢复执行
  • 整个过程,线程没有被阻塞

我曾经在面试时被问到:「挂起函数和普通函数有什么区别?」我的回答是:普通函数是「一条路走到黑」,挂起函数是「走到一半可以停下来喝口水,喝完继续走」。

避坑指南:挂起函数里不要做太耗 CPU 的计算。挂起是为了等待 I/O,不是让你做死循环。如果真有大量计算,用 withContext(Dispatchers.Default) 切到后台线程。

17.5 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以对照着回顾一下:

Kotlin 协程 协程作用域 GlobalScope runBlocking 协程构建器 launch async 挂起函数 suspend 关键字 核心要点 • 协程是轻量级线程,支持挂起与恢复 • GlobalScope 全局作用域,runBlocking 阻塞当前线程 • launch 不返回结果,async 通过 await 获取结果

这张图里,我把协程作用域、构建器、挂起函数三大块串起来了。你从中心出发,沿着箭头走一遍,基本就能理清本章的知识脉络。

好了,协程基础就讲到这里。记住:多写、多试、多踩坑,才能真正理解协程的妙处。我当年也是从「这什么鬼」到「真香」一步步过来的。


公众号:蓝海资料掘金营,微信deep3321