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 和协程作用域时各种灵异崩溃。