如何理解协程挂起

900 阅读2分钟

“挂起”并不是“阻塞”,而是告诉协程:“我现在没法继续了,你先去干别的吧,等我准备好了再回来继续执行。”

举个类比:打电话订外卖

假设你是主线程(UI线程):

  • 你打电话(调用一个挂起函数)订外卖。
  • 如果你“阻塞”,就像你一直拿着电话等对方送来外卖,你啥都干不了(UI卡住)。
  • 如果你“挂起”,就像你告诉电话:“我等外卖的结果,你记得回来通知我。” 然后你继续干别的去了(UI没卡)。
  • 外卖来了(挂起函数完成),电话通知你回来继续处理剩下的代码(恢复协程)。

在代码中的表现

launch {
    println("A. 开始请求")
    val result = getDataFromNetwork()  // 假设是个挂起函数
    println("B. 获取结果: $result")
}

如果 getDataFromNetwork()是挂起函数:

  • 当遇到 getDataFromNetwork():
    • 如果结果还没返回,协程就“挂起”了(让出当前执行权)。
    • 当前线程(可能是主线程)不会阻塞,可以干别的。
  • 网络请求返回后,协程恢复执行,从 result = ... 后继续走。

🔍 用协程状态图理解

协程状态变化大致如下:

CREATED -> RUNNING -> SUSPENDED (挂起) -> RUNNING -> COMPLETED

例如:

val job = launch {
    val data = fetchData() // suspend 函数
    println("数据:$data")
}

当执行到 fetchData():

  • 协程进入 SUSPENDED 状态。
  • 网络请求完成后,恢复为 RUNNING。
  • 最后执行完所有逻辑,变成 COMPLETED。

❗“挂起”≠“阻塞”

操作类型是否占用线程是否影响 UI
阻塞(阻塞线程)✅ 是❌ 卡 UI
挂起(让出协程)❌ 否✅ 不卡 UI

比如:

// 挂起
withContext(Dispatchers.IO) {
    val data = getDataFromNetwork() // 这个挂起,不阻塞线程
}

// 阻塞(⚠️ 不推荐)
Thread.sleep(3000) // 阻塞线程,会卡住当前线程(可能是 UI)

🧪 深入底层:挂起是编译器级魔法

挂起函数在编译后会被“转换”成带有 回调状态机 的形式,执行到挂起点时,它会:

  • 保存当前函数执行的中间状态(寄存器、变量、堆栈位置等)
  • 挂起自己,让出线程
  • 等待异步任务完成后,恢复到保存的状态,继续运行后续代码

这就是为什么协程看起来是同步写法,实质是异步回调。

✅ 小结

理解点内容
挂起 ≠ 阻塞挂起是暂停协程执行,不会阻塞线程
恢复执行挂起点完成后,协程从挂起点继续执行
编译器魔法挂起函数被转化为带状态机的“异步流程”
写法像同步实际是异步非阻塞执行,背后是协程调度器管理