Kotlin 协程基础概念
开始讲协程之前,我想先聊点实在的。你平时写异步代码是不是经常用回调?回调地狱、线程切换、异常处理……写多了真的很累。协程就是来解决这些痛点的。但很多人一开始就被“挂起”“非阻塞”“轻量级”这些词搞懵了。别急,我们慢慢拆开看。
1.1 挂起函数:本质是“可暂停的”普通函数
挂起函数就是用 suspend 关键字修饰的函数。它的特殊之处在于:可以在执行中途暂停,把线程让出来,等某个条件满足后再恢复执行。嗯,说白了,就是函数“挂起”了,但线程没闲着。
关键认知:挂起 ≠ 阻塞。阻塞是线程在那干等,挂起是线程回线程池继续干别的活了。
举个例子,你写一个网络请求:
suspend fun fetchData(): String {
// 模拟耗时
delay(1000)
return "数据"
}
调用 delay(1000) 时,当前协程挂起,但执行它的线程(比如主线程或后台线程)并不会卡住。我刚开始写协程时总把挂起当阻塞,心想那跟 Thread.sleep 有啥区别?实际跑一下线程信息才明白——挂起时线程早已切去干别的活了,这就是协程调度器的功劳。
记得我曾经在一个网络拦截器里调用了 delay,结果导致整个 OkHttp 请求队列卡住,排查半天发现是挂起函数用错了地方。这是后话,后面章节会细讲。
1.2 协程与线程的区别:一张表格说清楚
协程和线程经常被拿来比较。我觉得最形象的说法是:线程是操作系统调度的,协程是代码层面自己调度的。你想想看,创建一个线程要跟内核交互,开销很大;而协程就是一个对象,几纳秒就搞定了。
| 对比维度 | 线程 | 协程 |
|---|---|---|
| 创建开销 | 高(KB 级栈、内核对象) | 极低(几个对象引用) |
| 切换代价 | 内核态切换(上下文保存) | 用户态切换(函数调用级别) |
| 调度方式 | 抢占式(OS 决定) | 协作式(开发者决定挂起点) |
| 并发数量 | 一般几千个就到顶了 | 几十万随便开 |
| 阻塞行为 | 线程会卡住 | 挂起后线程不会被占用 |
这个表格我特别喜欢在团队评审时拿出来。以前我们项目用线程池做并发,一不注意就 OOM 了。后来切到协程,同样逻辑,代码量减了一半,性能还稳了。注意,不是协程比线程快,而是它省去了大量线程切换闲工夫。
1.3 协程的轻量级特性:十万协程不是梦
为什么协程能开那么多?因为它在内存里就是一个状态机。每个协程只占几百字节(相比线程的 MB 级栈,几乎可以忽略)。我曾经在一个测试项目里,单线程上启动 10 万个协程,每个协程只干一件小事——延时 100ms 后打印一条日志。结果内存才几十 MB,CPU 占用也平稳。如果换成线程,早就爆了。
注意:轻量不意味着无限制。虽然协程对象小,但如果每个协程内部持有大对象引用、或者用 Dispatchers.IO 时启动太多协程,仍然会导致内存压力。我见过有人一把启动 10 万个协程去访问同一台服务器,结果直接打挂了服务端。
轻量级带来的另一个好处是:你可以用协程写出非常直觉的顺序代码。挂起函数让异步调用看起来像同步一样。这背后就是协程编译器帮我们生成了 Continuation 类,自动处理挂起和恢复。
// 看似顺序执行,实际是异步挂起
suspend fun loadUser() {
val user = api.fetchUser() // 挂起
val avatar = api.fetchAvatar(user.id) // 挂起
showUI(user, avatar)
}
这段代码如果不用协程,用回调得写两三层嵌套。现在你看着就是普通顺序调用,对吧?这就是协程的威力——轻量、可挂起、可恢复。
1.4 本章知识体系图
下面这张图把三个核心概念串在了一起。你可以看到挂起函数是协程的“血液”,线程是协程运行的“跑道”,而轻量级特性是协程能大规模使用的根本保障。
个人小技巧:如果你刚接触协程,先别急着深入源码。把挂起函数、线程调度、轻量级这三件事的关系捋顺,后面排查泄漏问题时就心里有底了。我曾经就是一开始概念不清,导致后来混用 OkHttp 和协程作用域时各种灵异崩溃。