协程基础:概念与优势

1. 什么是协程?

协程,说白了就是一段可以被挂起、稍后又能从挂起点继续执行的代码块。它不是线程,而是运行在线程上的“轻量级任务”。我刚开始接触 Kotlin 协程时,经常把它和线程搞混,觉得不就是并发的另一种写法吗?后来才意识到,它最核心的价值不在于“并发”,而在于“挂起不阻塞线程”。

举个例子:你发出一个网络请求,线程等着响应。传统写法你可能会用回调或者阻塞式 IO。协程的做法是——挂起当前协程,释放线程去干别的活,等数据回来再自动恢复执行。线程没闲着,协程也没停,只是两者解耦了。

个人心得:有一次我在项目里用协程处理上百并发的 API 请求,线程池只配了 4 个线程,扛得稳稳的。如果是回调嵌套,那代码早就变成“回调地狱”了。

2. 挂起与恢复

挂起函数(suspend 关键字标记)是协程的基础。编译器会把挂起函数改造成状态机,每次挂起就是一个状态点。说白了,Kotlin 在背后帮你维护着一个 Continuation 对象,记录着执行到哪一步、局部变量是什么。

suspend fun fetchData(): String {
    val result = withContext(Dispatchers.IO) {
        // 模拟网络请求,挂起但不阻塞线程
        delay(1000)
        "data from server"
    }
    return result
}

你想想看,delay() 不是 Thread.sleep()。后者让当前线程睡 1 秒,线程被占着不能干别的;前者挂起协程,线程马上回去做其他协程的活。这就是“轻量”的根本原因。

我曾经在线上环境排查过一个 ANR,就是因为有人在 Dispatchers.Main 上用了 Thread.sleep() 来模拟延迟——活生生把 UI 线程卡住了。换成协程的 delay() 后,问题立刻消失。

注意:挂起函数只能在协程或者另一个挂起函数里调用。普通函数里写了 delay() 是编译不过的。

3. 结构化并发

这是 Kotlin 协程最让我喜欢的设计。简单说就是:协程有父子层级,父协程取消,所有子协程自动取消;父协程等待所有子协程完成后才会结束。这完美避免了手动管理生命周期带来的泄漏风险。

fun main() = runBlocking {
    val parentJob = launch {
        launch {
            delay(2000)
            println("child 1 done")
        }
        launch {
            delay(1000)
            println("child 2 done")
        }
    }
    delay(500)
    parentJob.cancel()  // 两个子协程都会被取消
}

我习惯把 CoroutineScope 看作一个“容器”,里面装着所有协程。在 Android 里,Activity 或 Fragment 销毁时取消对应的 viewModelScope 或 lifecycleScope,协程就自动清理了。可惜很多人没用好,导致协程泄漏——这就是咱们这个课程要解决的核心问题。

4. 协程的优势

特性 线程 协程
创建开销 ~1 MB 内核栈 几十字节堆内存
上下文切换 内核调度,耗时 用户态挂起,纳秒级
可取消性 需要 Interrupt 机制,麻烦 内置 cancellation 协程体
结构化生命周期 无 通过 Scope 自动管理
与回调的互操作性 需手动包装 suspendCancellableCoroutine 优雅转换

你看这个表,协程在几乎所有维度都占优。但它不是银弹——比如 CPU 密集型计算,协程挂起并不能并行,还是得靠多线程。不过对于 IO 密集型任务(网络请求、数据库读写),协程可以说是最佳选择。

5. 使用场景与注意事项

实际项目中,协程用得最频的地方就是网络请求。我们用 Retrofit + 协程,suspend fun 直接返回结果,不用层层回调。但这里有个陷阱:

协程泄漏风险:如果请求发出去后,页面销毁了,但协程没有被取消,它会继续持有 View 或 Context 的引用,导致内存泄漏。更严重的是,如果请求回调里还在更新 UI,就会抛出崩溃。

为了避免这种情况,lifecycleScope 自动帮我们管理了生命周期——页面 onDestroy 时,作用域自动取消。但如果你自己创建了 CoroutineScope 却忘了取消,或者用了 GlobalScope,麻烦就来了。

我遇到过一位同事,在 RecyclerView 的 ViewHolder 里用 GlobalScope.launch 发起网络请求,结果列表快速滑动时,创建了几十个协程同时跑,响应回来时 View 已经回收了,直接导致 IllegalStateException。从那以后,我要求团队:所有协程必须绑定生命周期,严禁使用 GlobalScope。

避坑指南:在 OkHttp 的拦截器或回调里启动协程时,一定要记得传入外部 Scope 或使用 withContext 配合 NonCancellable。我们后面章节会详细讲这个案例。
协程核心概念结构图 CoroutineScope(结构化容器) launch { ParentJob } 子协程 A(请求1) 子协程 B(请求2) 挂起(delay/IO) 恢复 & 完成 取消 / 泄漏风险 挂起 → 恢复 挂起不阻塞线程 自动恢复执行 子协程随父取消

这张图展示了协程在 CoroutineScope 内的生命周期:父协程启动两个子协程,子协程可以挂起(如网络 IO),随后恢复完成。如果父协程被取消,或作用域结束,所有子协程都会被取消——这正是避免协程泄漏的关键机制。