协程基础概念:suspend、构建器与挂起恢复

说实话,协程刚出来那会儿,我看源码也是一脸懵。什么挂起、恢复、suspend关键字,感觉像魔法一样。但后来调了几次泄漏问题,才真正理解这些基础概念有多重要。

嗯,这一章我们就聊聊协程最核心的三个东西:suspend函数、协程构建器(launch/async),以及挂起与恢复机制。把这些搞懂了,后面排查泄漏才能有的放矢。

1. suspend函数:协程的基本操作单元

suspend函数,说白了就是给编译器一个信号:这个函数可能会暂停执行。我在项目中遇到过不少同事,以为suspend就是“阻塞线程”,其实完全不是一回事。

看个最简单的例子:

suspend fun fetchData(): String {
    // 模拟网络请求
    delay(1000L)  // 挂起1秒,但不阻塞线程
    return "数据加载完成"
}

关键点在于:

  • suspend函数只能在协程内部或其他suspend函数中调用。普通函数里调不了,编译器会直接报错。
  • 它不会阻塞当前线程。线程在协程挂起期间可以去做别的事,比如处理其他协程。
  • 本质上是一个回调的语法糖。编译器会把suspend函数转换成状态机,每次挂起就是一个状态点。
我个人习惯把suspend函数看成“可暂停的普通函数”。它只不过多了个Continuation参数,用来告诉调用方:我暂停到哪了,该怎么恢复。

2. 协程构建器:launch与async

协程构建器就是启动协程的入口。我早期用Kotlin协程时,经常搞混这两个家伙,结果出了几次泄漏,不得不重构代码。

构建器 返回值 用途 异常处理
launch Job 启动一个不返回结果的协程(类似线程的start) 异常默认传递给父协程
async Deferred<T> 启动一个返回结果的协程(类似Callable) 异常在调用await()时抛出

launch 是最常用的。你只是想“干一件事”,不关心结果,就用它。比如更新UI、写日志。

async 适合并发执行任务,然后统一汇总结果。但要注意,async不是单纯的并行,它只是启动了协程,调用await()才挂起等待结果。

// launch 示例:发起一个网络请求,完成后更新UI
scope.launch {
    val result = fetchData()
    updateUI(result)  // 这里不需要返回值
}

// async 示例:并发请求两个接口,汇总数据
scope.launch {
    val deferred1 = async { fetchUserInfo() }
    val deferred2 = async { fetchUserPosts() }
    val user = deferred1.await()
    val posts = deferred2.await()
    showProfile(user, posts)
}
我曾经犯过一个低级错误:在非协程作用域里直接调async。结果编译没问题,但运行时就抛异常。记住,async也必须放在协程作用域里。

3. 挂起与恢复机制:协程的“断点续传”

挂起和恢复,是协程最神奇的地方。你想想看,一个函数执行到一半,停下来去做别的事,过一会儿再回来接着干——这怎么实现的?

其实原理不复杂:编译器把suspend函数改写成一个状态机。每个挂起点都对应一个状态。协程挂起时,把当前的状态和局部变量保存下来;恢复时,根据状态跳到对应的位置继续执行。

举个生活中的例子:你在餐厅点餐。传统线程模型是你站在柜台前一直等着(阻塞线程)。协程模型是你先点餐,拿个号,然后回座位玩手机(挂起)。等餐好了,广播叫你的号,你再去取餐(恢复)。

关键流程:

  1. 调用suspend函数:编译器检查是否有挂起点。
  2. 遇到挂起点(比如delay、网络请求):保存当前上下文(Continuation对象)到堆栈。
  3. 让出线程:线程返回执行其他协程或任务。
  4. 条件满足后(比如延迟结束、数据返回):调度器把协程重新加入执行队列。
  5. 恢复执行:从保存的挂起点继续往下走。
挂起不是“暂停线程”,而是“保存状态、让出线程”。线程还是那个线程,只是协程切换了。这才是协程高效的根本原因——没有线程切换的开销。

4. 协程的生命周期:Job和Deferred

每次启动一个协程,都返回一个Job或Deferred对象。它们代表了协程的生命周期。我排查泄漏时,第一个查的就是这些对象有没有被正确取消。

// Job 的状态流转
val job: Job = scope.launch {
    // ...
}
job.cancel()  // 取消协程
job.join()    // 等待协程结束

// Deferred 是 Job 的子接口,多了 await()
val deferred: Deferred<String> = scope.async {
    "结果"
}
val result = deferred.await()

Job的状态包括:New、Active、Completing、Completed、Cancelling、Cancelled。搞清楚这些状态,就能知道协程到底泄漏在哪一步。

我调试协程泄漏时,喜欢在取消逻辑里加日志,输出Job的isActive、isCancelled、isCompleted状态。这样一眼就能看出协程是不是已经“死了”但还被引用着。

5. 概念关系图

下面这张图,把这一章的核心知识点串了一遍。你看一遍,再回头看书,会清晰很多。

协程基础概念关系图 协程(Coroutine) suspend 函数 协程构建器 挂起与恢复 可暂停但不阻塞线程 launch vs async 状态机 + Continuation 理解这三者的关系,是排查协程泄漏的第一步 挂起让出线程,恢复继续执行——线程没变,只是协程切换了 Job / Deferred 生命周期管理

嗯,suspend函数、协程构建器、挂起恢复机制——这三个点就像协程的三根支柱。你理解得越深,排查泄漏时就越能一眼看出问题在哪。

我记得第一次接触协程时,觉得挂起恢复特别玄乎。后来自己画了张状态机图,才恍然大悟:其实就是编译器帮你写了个switch-case。每个挂起点就是一个case,你用上了没有挂起的调用,就跳出这个状态机,那协程当然就乱掉了。