协程本质与挂起函数
协程(Coroutines)通常被描述为“轻量级线程”。虽然这个比喻有助于理解其用途,但从技术本质上讲,协程是一种由编译器和运行时共同协作实现的协作式多任务处理机制。
协程 vs. 线程
协程与线程有着本质的区别,主要体现在资源的占用和调度方式上。
| 特性 | 线程 (Thread) | 协程 (Coroutine) |
|---|---|---|
| 操作系统资源 | 属于内核态资源,由 OS 调度 | 属于用户态资源,由程序 (运行时) 调度 |
| 内存消耗 | 栈空间通常为 1MB 左右 | 仅几十字节(Continuation 对象) |
| 上下文切换 | 涉及 CPU 寄存器和内核栈切换,开销大 | 仅为函数对象引用切换,开销极小 |
| 并发量 | 受限于内存,通常几千个即达瓶颈 | 可以轻松启动百万级协程 |
挂起函数:非阻塞的魔法
suspend 关键字是协程系统的核心原语。一个挂起函数代表了一个可以被暂停执行并在稍后恢复的操作。
挂起不等于阻塞
核心误区
很多初学者认为 suspend 意味着函数运行在后台。错误! suspend 关键字本身并不执行异步操作,它只是给了函数“挂起”的能力。
- 阻塞 (Blocking):线程被占用,无法干别的事(就像排队办业务,人在窗口等着)。
- 挂起 (Suspending):线程被释放,去执行其他协程(就像办业务时资料不全,领个号先去喝咖啡,等资料齐了再被叫回来)。
挂起的视觉错觉
协程最大的魅力在于:用同步的代码风格编写异步逻辑。
kotlin
suspend fun updateUI() {
val user = fetchUser() // 挂起 1: 释放线程去处理 UI 刷新
showUser(user) // 恢复 1: 自动回到当前上下文执行
}在编译器视角下,上面的代码会被拆分为多个阶段,每个阶段都是一个 Continuation 节点。
协程的运行生命周期
一个协程从创建到销毁通常经历以下阶段:
- 创建 (Created):通过
launch/async构建出Continuation状态机。 - 分派 (Dispatched):调度器将其放入执行队列。
- 运行 (Running):线程执行其字节码。
- 挂起 (Suspended):遇到挂起点,保存现场并退出线程。
- 恢复 (Resumed):条件满足(如 IO 完成),重新放回调度队列。
- 结束 (Completed):执行完毕或被取消。
核心准则总结
- 不要在主线程调用会阻塞的函数:协程能解决挂起问题,但无法直接解决代码中的阻塞 API 调用(如旧的 Socket 操作)。
- suspend 函数是“协程亲和”的:它们会自动识别当前的协程环境并协作。
- 理解“轻量”的边界:虽然协程轻量,但其承载的业务逻辑(如持有大量大对象)依然会消耗内存。