Kotlin 协程原理

协程 是 kotlin 提供的轻量级并发编程方法 , 核心是"非阻塞挂起 ",能够让我们用同步的方式写出异步的代码,以简化异步逻辑的代码编写,同时避免陷入回调地狱。


协程与线程的区别

线程是操作系统调度的基本单位,协程是运行在线程之上的轻量级任务。

两者最大的区别有以下几点:

  1. 调度方式不同

    • 线程由操作系统内核调度,线程切换涉及用户态和内核态切换,成本较高。
    • 协程通常由语言运行时或协程框架调度, Kotlin 协程通过状态机和 Continuation 保存执行状态,所以切换成本通常更低。
  2. 资源占用不同

    • 每个线程都需要独立的线程栈,创建大量线程会占用较多内存,内存开销在 MB 级别,因此不能无限创建
    • 协程内存开销在 KB 级别,并且不需要一对一绑定线程,一个线程可以运行大量协程,所以协程更加轻量。
  3. 阻塞行为不同

    • 线程执行 Thread.sleep() 或阻塞式 IO 时,整个线程无法执行其他任务;
    • 协程执行 delay() 或真正的挂起函数时,只暂停当前协程,底层线程可以继续执行其他协程。
  4. 并行能力不同

    • 协程本身不等于并行。多个协程如果运行在同一个线程上,只能并发执行;只有它们被调度到多个线程或多个 CPU 核心上时,才可能真正并行。

总结来说:

线程是实际执行代码的资源,协程是在线程上被调度执行的任务;协程不能替代线程,它是对线程资源更高效的一种使用方式。

协程一定比线程快吗?

不一定。协程的优势主要是**降低大量异步任务的资源占用和调度成本**,而不是让单个计算任务执行得更快。对于 CPU 密集型任务,最终仍然受到 CPU 核心数和线程数量的限制。如果协程内部执行了阻塞代码,同样可能阻塞底层线程。

一个线程可以运行多少个协程?

一个线程可以先后运行大量协程,但同一时刻一个线程只能执行一个协程的代码。协程执行到挂起点后会让出线程,线程再执行其他协程,因此形成并发效果。

原理

可以把 Kotlin 协程理解成:

复制代码
编译器生成的状态机
+
Continuation 保存执行现场
+
Dispatcher 决定在哪个线程恢复
+
Job 管理生命周期和取消
+
CoroutineScope 建立结构化父子关系

协程底层的本质,是编译器帮我们做了一次 CPS 变换(Continuation Passing Style) 。具体来说,任何一个 suspend 函数,编译器都会在它背后偷偷增加一个 Continuation 类型的参数,并把返回值类型变成 Any?。如果函数直接执行完,就返回结果;如果遇到挂起点需要等待,就返回一个单例对象 COROUTINE_SUSPENDED 标记,表示当前协程被暂停了。

同时,编译器会把函数体转换成一个状态机 。函数里每一个挂起点,都被切分成状态机的不同状态,用一个 label 字段来记录当前执行到了哪个 case。所有需要跨挂起点的局部变量,都会被存到这个 Continuation 对象里,不会丢失。

当异步操作(比如网络或 delay)还没完成时,协程就保存当前状态并退出调用栈 ,把线程让出来去执行别的协程,这就是'非阻塞挂起'。等结果回来了,异步回调会调用 Continuation.resumeWith,这时 Dispatcher 会决定把剩余状态放到哪个线程继续执行------Main 就抛回主线程,IO 就丢进线程池。

说白了,协程就是在用户态模拟了线程的'暂停-恢复'能力,但把切换成本降到了极致
#mermaid-svg-xio1F9pcM15s5IhI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xio1F9pcM15s5IhI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xio1F9pcM15s5IhI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xio1F9pcM15s5IhI .error-icon{fill:#552222;}#mermaid-svg-xio1F9pcM15s5IhI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xio1F9pcM15s5IhI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xio1F9pcM15s5IhI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xio1F9pcM15s5IhI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xio1F9pcM15s5IhI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xio1F9pcM15s5IhI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xio1F9pcM15s5IhI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xio1F9pcM15s5IhI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xio1F9pcM15s5IhI .marker.cross{stroke:#333333;}#mermaid-svg-xio1F9pcM15s5IhI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xio1F9pcM15s5IhI p{margin:0;}#mermaid-svg-xio1F9pcM15s5IhI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xio1F9pcM15s5IhI .cluster-label text{fill:#333;}#mermaid-svg-xio1F9pcM15s5IhI .cluster-label span{color:#333;}#mermaid-svg-xio1F9pcM15s5IhI .cluster-label span p{background-color:transparent;}#mermaid-svg-xio1F9pcM15s5IhI .label text,#mermaid-svg-xio1F9pcM15s5IhI span{fill:#333;color:#333;}#mermaid-svg-xio1F9pcM15s5IhI .node rect,#mermaid-svg-xio1F9pcM15s5IhI .node circle,#mermaid-svg-xio1F9pcM15s5IhI .node ellipse,#mermaid-svg-xio1F9pcM15s5IhI .node polygon,#mermaid-svg-xio1F9pcM15s5IhI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xio1F9pcM15s5IhI .rough-node .label text,#mermaid-svg-xio1F9pcM15s5IhI .node .label text,#mermaid-svg-xio1F9pcM15s5IhI .image-shape .label,#mermaid-svg-xio1F9pcM15s5IhI .icon-shape .label{text-anchor:middle;}#mermaid-svg-xio1F9pcM15s5IhI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xio1F9pcM15s5IhI .rough-node .label,#mermaid-svg-xio1F9pcM15s5IhI .node .label,#mermaid-svg-xio1F9pcM15s5IhI .image-shape .label,#mermaid-svg-xio1F9pcM15s5IhI .icon-shape .label{text-align:center;}#mermaid-svg-xio1F9pcM15s5IhI .node.clickable{cursor:pointer;}#mermaid-svg-xio1F9pcM15s5IhI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xio1F9pcM15s5IhI .arrowheadPath{fill:#333333;}#mermaid-svg-xio1F9pcM15s5IhI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xio1F9pcM15s5IhI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xio1F9pcM15s5IhI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xio1F9pcM15s5IhI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xio1F9pcM15s5IhI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xio1F9pcM15s5IhI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xio1F9pcM15s5IhI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xio1F9pcM15s5IhI .cluster text{fill:#333;}#mermaid-svg-xio1F9pcM15s5IhI .cluster span{color:#333;}#mermaid-svg-xio1F9pcM15s5IhI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xio1F9pcM15s5IhI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xio1F9pcM15s5IhI rect.text{fill:none;stroke-width:0;}#mermaid-svg-xio1F9pcM15s5IhI .icon-shape,#mermaid-svg-xio1F9pcM15s5IhI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xio1F9pcM15s5IhI .icon-shape p,#mermaid-svg-xio1F9pcM15s5IhI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xio1F9pcM15s5IhI .icon-shape .label rect,#mermaid-svg-xio1F9pcM15s5IhI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xio1F9pcM15s5IhI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xio1F9pcM15s5IhI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xio1F9pcM15s5IhI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

开发者编写 suspend fun
Kotlin 编译器进行 CPS 转换
生成状态机,并增加 Continuation 参数
调用 suspend 函数
传入 Continuation 对象

包含 label 和局部变量
执行状态机 switch(label)
执行当前状态对应的代码
是否遇到挂起点?

例如 delay 或网络请求
直接返回执行结果
协程执行结束
保存当前执行现场
更新 label 为下一个状态
将局部变量保存到 Continuation
启动异步任务

并传入 Continuation
返回 COROUTINE_SUSPENDED
释放当前线程的执行权

线程可以执行其他任务
异步任务执行完成
调用 Continuation.resumeWith
Dispatcher 决定恢复线程

例如 Dispatchers.Main
在目标线程恢复协程
状态机根据 label

跳转到对应位置

例子

原代码:

kotlin 复制代码
suspend fun load() {
    println("A")

    val user = api.getUser()

    println("B: $user")
}

概念上会被转换为:

kotlin 复制代码
fun load(continuation: LoadContinuation): Any? {
    when (continuation.label) {
        0 -> {
            println("A")

            continuation.label = 1
            val result = api.getUser(continuation)

            if (result === COROUTINE_SUSPENDED) {
                return COROUTINE_SUSPENDED
            }

            continuation.user = result
        }

        1 -> {
            val user = continuation.result
            println("B: $user")
        }
    }

    return Unit
}

这不是编译器生成代码的完整形式,但表达了核心原理:

复制代码
label = 0:第一次执行label = 1:从 getUser() 后面恢复

需要跨越挂起点保存的局部变量,也会存入 Continuation 对象。

暂停过程

执行到:

复制代码
val user = api.getUser()

如果结果暂时不可用:

  1. 保存当前状态和局部变量。
  2. label 设置为下一个状态。
  3. 返回特殊标记 COROUTINE_SUSPENDED
  4. 当前线程继续执行其他任务。

这里暂停的是:

复制代码
协程

不是:

复制代码
线程

恢复过程

网络结果返回后,相关异步 API 会执行类似:

复制代码
continuation.resume(user)

最终调用:

复制代码
continuation.resumeWith(Result.success(user))

之后:

  1. Dispatcher 检查应该在哪个线程恢复。
  2. 将恢复任务放入目标线程或线程池的队列。
  3. 状态机再次执行。
  4. 根据 label 跳转到挂起点之后。
  5. 继续执行 println("B")

Continuation.resumeWith() 正是用于把结果或异常传回挂起点,并恢复协程执行。

整体流程

text 复制代码
协程开始
   ↓
执行到挂起函数
   ↓
结果是否立即可用?
   ├── 是 → 直接继续
   └── 否
        ↓
     保存状态
        ↓
     返回 COROUTINE_SUSPENDED
        ↓
     释放当前线程
        ↓
     异步结果到达
        ↓
     resumeWith(result)
        ↓
     Dispatcher 调度
        ↓
     从下一个状态继续

suspend 关键字做了什么?

  • suspend 是给编译器看的。它表示该函数可能包含挂起点。编译后,函数会增加一个 Continuation 参数,并使用普通结果或者 COROUTINE_SUSPENDED 作为返回值。
  • 对于包含挂起点的函数,编译器通常会生成状态机来保存和恢复执行状态

Continuation 是什么?

  • Continuation 可以理解为协程后续执行过程的抽象,它保存 CoroutineContext,并通过 resumeWith(Result<T>) 接收成功结果或异常。对于编译器生成的状态机,Continuation 对象还会间接保存当前状态以及跨挂起点需要使用的局部变量。
kotlin 复制代码
interface Continuation<in T> {
    val context: CoroutineContext

    fun resumeWith(result: Result<T>)
}

谁负责恢复协程?

  • 产生异步结果的组件负责触发 Continuation,例如定时器、网络回调或者其他协程;它调用 resumeresumeWith 提交结果。之后是否需要切换线程、在哪个线程继续执行,则由 Dispatcher 和 Continuation 拦截机制负责。