1. Kotlin协程简介

协程这个概念,说白了就是一套「轻量级的并发方案」。我第一次接触协程时也困惑过:这不就是线程封装吗?后来在项目中踩过坑才明白——它本质上是一种可挂起、可恢复的计算过程。

协程是什么?

Kotlin的协程是一种协作式多任务机制。它允许你在代码中暂停一个任务,然后稍后恢复。注意,这里的「暂停」不是阻塞线程。我习惯这样理解:协程运行在线程上,但挂起时它会释放线程,让线程去做其他事。等条件满足,再回来继续。

  • 轻量级:一个线程可以运行成千上万个协程。
  • 非阻塞:挂起时不会占用线程。
  • 结构化并发:通过CoroutineScope管理生命周期。

CoroutineScope:协程的作用域

每个协程都必须在一个作用域内启动。CoroutineScope定义了协程的上下文和生命周期。

val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
    // 协程体
}

我个人习惯用viewModelScope或lifecycleScope来绑定界面生命周期。我曾经在写网络请求时忘记取消协程,导致界面销毁后还在回调,直接崩了。后来我严格限制作用域,再没出过问题。

Continuation:协程的“档位”

Continuation是协程内部的挂起恢复机制。每次遇到挂起函数,协程会把当前状态打包成一个Continuation对象保存起来。等条件就绪,就通过这个对象恢复执行。

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

你可能会问:这东西跟我写代码有什么关系?嗯,关系不大,但理解它有助于你排查堆栈里的奇怪调用。我看到过不少崩溃日志里出现Continuation,明白了原理就很容易定位。

协程 vs 线程

很多新手喜欢把协程当成“超级线程”。其实它们差别很大。我整理了一张对比表:

特性协程线程
启动成本极低(栈在堆中)较高(分配栈空间)
切换开销用户态切换,几乎无开销内核态上下文切换
阻塞挂起不阻塞线程阻塞会占用系统资源
调度协作式,主动挂起抢占式,由系统调度
并发数轻松上万数千即需谨慎

说实话,我刚开始用协程时,还是在用线程那套思维写代码:用delay替代Thread.sleep而已。后来遇到耗时的计算任务,直接在协程里写了个循环,结果卡住了整个线程。我才意识到:协程并不会自动把密集计算放到后台线程,你得配合withContext(Dispatchers.Default)才行。

核心观点:协程是线程的「管理者」,不是替代品。它让你用同步的写法处理异步逻辑。

协程启动的基本方式

Kotlin提供了launch和async两种启动方式:

// launch:返回Job,不关心结果
scope.launch {
    delay(1000)
    println("Done")
}

// async:返回Deferred,等待结果
val deferred = scope.async {
    delay(1000)
    "Result"
}
val result = deferred.await()

launch适合“发后不管”,async适合需要返回值的场景。我建议优先用launch,除非你明确需要并发拿结果。

小技巧:在Android的ViewModel里,用viewModelScope.launch就行,它会自动绑定ViewModel生命周期。

注意:不要在launch块里直接调用Thread.sleep,那会阻塞整个线程。要用delay,它是挂起函数。

Kotlin协程执行流程 CoroutineScope launch / async 挂起函数 Continuation(保存状态) 恢复执行 完成 每个协程必须由一个 CoroutineScope启动 挂起时Continuation保存 当前上下文和恢复位置 不阻塞线程

这张图梳理了协程执行的核心流程。从CoroutineScope开始,经过launch/async启动,执行挂起函数时,Continuation会保存现场,之后可以选择恢复执行或直接完成。注意,挂起期间线程可以干别的事。

总结

这一章我们讲了协程的基本概念、CoroutineScope的作用、Continuation的底层机制,以及和线程的对比。协程不是银弹,但用对了地方,能极大简化并发代码。记住:结构化并发是Kotlin协程的精华,好好利用作用域,避免泄露。