Skip to content

Kotlin 协程原理详解(挂起 / 恢复 / 取消 / 调度 / 异常处理 全面梳理)

1. 整体架构 — 协程是什么(用一句话)

Kotlin 协程是一套轻量级的用户态协作式任务框架,借助编译器把 suspend 函数变成带 Continuation 的状态机,使得异步逻辑可以像同步代码一样写,但底层不阻塞线程:挂起点会保存执行状态并返回,恢复时通过 Continuation.resumeWith(...) 继续执行。


2. 核心概念(名词速查)

  • suspend 函数:可以在其中“挂起”执行的函数(编译器转成状态机并隐式接受 Continuation<T>)。
  • Continuation<T>:协程的“恢复句柄”,接口包含 resumeWith(Result<T>)。保存了要恢复时的上下文和下一步执行信息。
  • CoroutineContext:协程运行时的集合(JobCoroutineDispatcherCoroutineNameCoroutineExceptionHandler 等元素)。
  • Job / SupervisorJob:协程的生命周期句柄,支持 cancel()join()、父子关系与结构化并发策略。
  • CoroutineDispatcher:如何把挂起/恢复的执行调度到线程(或线程池)上(例如 Dispatchers.DefaultIOUnconfined、自定义 dispatcher)。
  • suspendCoroutine / suspendCancellableCoroutine:原语,用于从回调/回调式 API 构造挂起点(可取消版本会在 Job 取消时触发回调)。
  • Structured Concurrency:父协程作用域管理子协程,保证不会遗留孤儿协程,异常/取消沿 Job 层级传播。
  • CancellationException:取消以异常形式传播但通常被当作正常控制流处理(不会视为未捕获异常)。

3. 编译器视角:suspend 如何实现

suspend fun foo(): T 在 JVM 上被编译器改写为: foo(Continuation<T> continuation) —— 即把恢复点(状态)封装到 Continuation 中。编译器将 suspend 点拆成状态机(类似 Kotlin 的协程状态机或 JavaScript transpile 的异步状态机):

  • 每遇到 suspend 点,函数会把当前局部变量和下一步状态保存在 Continuation(或其生成的子类)里,然后返回(通常是 COROUTINE_SUSPENDED 标记)。
  • 当某处恢复该 ContinuationresumeWith),状态机会根据保存的状态继续运行到下一个挂起点或结束。

直观理解:suspend 不是“阻塞线程”,而是把执行切片保存起来并把控制权交回运行时,运行时在合适时机把切片(Continuation)拿出来恢复。


4. 挂起(suspension)的两种常见实现方式

4.1 使用已有 suspend API

delay()withContext() 等是已经实现好的 suspend 函数;它们自身内部会决定是否立即返回 COROUTINE_SUSPENDED 并把 Continuation 保存到某个调度队列或计时器中。

4.2 从回调构建挂起点:suspendCoroutine 与 suspendCancellableCoroutine

kotlin
suspend fun awaitCallback(): String = suspendCoroutine { cont ->
  someAsyncApi { result, error ->
    if (error != null) cont.resumeWithException(error)
    else cont.resume(result)
  }
}
  • suspendCoroutine:不会自动与协程取消关联。若协程被取消,回调仍可能触发(要手动处理取消)。

  • suspendCancellableCoroutine

    • 提供 cont.invokeOnCancellation { ... } 注册取消回调,能在 Job 被取消时执行资源释放或取消底层调用(比如取消网络请求)。
    • 是构建“可取消挂起”的推荐方案。

5. 恢复(resume)机制

Continuation.resumeWith(result) 被调用时:

  1. ContinuationInterceptor(通常是 Dispatcher)可能拦截该 Continuation,把恢复动作封装并提交到线程池或 Runnable 队列;
  2. 恢复动作执行:恢复点的代码继续执行直到下一个 suspend 或结束;
  3. 如果 resumeWithResult 是失败(异常),会抛出异常到挂起点处,触发 try/catch 或由协程上下文处理。

重要:resumeWith 是线程安全的,且只应调用一次(重复调用会被忽略或抛出)。


6. 调度(Dispatchers)与 ContinuationInterceptor

  • CoroutineDispatcherContinuationInterceptor 的实现:负责将恢复动作调度到哪儿去运行。

  • 常见实现:

    • Dispatchers.Default —— 基于共享的工作窃取线程池(类似 ForkJoinPool)。
    • Dispatchers.IO —— 可扩展线程池用于阻塞 IO(大小高于 Default)。
    • Dispatchers.Main —— Android / UI 线程。
    • Dispatchers.Unconfined —— 不受特定线程约束:首次在调用线程运行,遇到挂起后恢复在发起恢复的线程(主要用于调试 / 某些特殊场景)。
  • 自定义 Dispatcher:实现 CoroutineDispatcher 并覆盖 dispatch(context, block) 即可。也可实现 Delay 接口提供定时调度支持。


7. 取消(Cancellation)原理与实践

7.1 基本原理:协作式取消

  • Kotlin 协程的取消是 *协作式- 的:Job.cancel() 会把 Job 标记为取消,但不会强行中断线程或强制停止执行体。运行中的协程需要在“可取消点”检测状态并主动结束。
  • 可取消点包括:suspend 函数(如 yield()delay()withContext())、select、以及你在自定义挂起点时手动检查 isActivecoroutineContext[Job]?.isCancelled

7.2 CancellationException

  • 取消会以 CancellationException 的形式传播(内部实现通常调用 resumeWithException(CancellationException()))。
  • CancellationException 被视为正常控制流:不会被 CoroutineExceptionHandler 视为未捕获致命异常(默认情况下)。在 try { ... } catch (e: Throwable) 时需注意捕获并区分。

7.3 suspendCancellableCoroutine 的优势

  • suspendCancellableCoroutinecont.invokeOnCancellation { } 中允许你在协程被取消时执行底层清理,比如取消一个 HTTP 请求。
  • 如果你使用 suspendCoroutine,并且底层回调不支持取消,你需要在回调中手动检查 isActivecont.context[Job]?.isCancelled

7.4 强制清理:NonCancellable

finally 中,如果你需要在取消时仍然执行某些挂起操作(比如关闭资源),可以用 withContext(NonCancellable) { ... }

kotlin
try {
  // work
} finally {
  withContext(NonCancellable) {
    // 安全执行挂起清理
  }
}

8. 结构化并发(Structured Concurrency)与 Job 层级

  • CoroutineScope.launch {}coroutineScope {} 等是结构化并发 API,父协程会等待其子协程完成(coroutineScope 会阻塞直到所有子协程结束)。
  • 父取消会传播给子:parent.cancel() 会取消子 Job(协作式)。
  • SupervisorJobsupervisorScope:允许子协程失败不影响同级其它子协程(即父不会因为单个子失败而取消其它子)。

示例对比:

kotlin
coroutineScope {
  launch { fail() }   // 如果抛异常,会取消整个 coroutineScope 的其它子协程
  launch { doWork() }
}

supervisorScope {
  launch { fail() }   // 失败不会影响其它同级子协程
  launch { doWork() }
}

9. 异常传播与 CoroutineExceptionHandler

  • 异常传播规则

    • 对于 根协程(root coroutine,直接由 GlobalScope.launch 等启动),未捕获的异常会由 CoroutineExceptionHandler 处理。
    • 对于 结构化并发(如 launchcoroutineScope 内),子协程未捕获异常会向上抛,最终导致父协程取消并传播到整个作用域,直至根或被捕获。
  • CoroutineExceptionHandler 只处理最终未被捕获且到达顶层的异常,不能被 async 引发的异常(async 抛出的异常在调用 await() 时才会抛出)。常见规则:

    • launch 的异常是“立即”传播的(会取消父),最终交由 CoroutineExceptionHandler 处理;
    • async 的异常会被封装在 Deferred,只有 await() 时抛出(也可视为“延迟异常”)。

10. 关键 API 的内部行为(withContext / async / launch / runBlocking)

  • withContext(dispatcher) { ... }:是个 suspend 函数,会切换 CoroutineContext(包括 Dispatcher),执行 block 并在 block 内可能挂起多次,直到 block 完成后返回结果。withContext 会把当前协程挂起并在新的 dispatcher 上恢复执行。
  • launch { ... }:立即返回 Job。内部创建 Continuation 并提交初始执行任务给 Dispatcher
  • async { ... }:返回 Deferred<T>(继承自 Job)。异常在 await() 时抛出(或在超出作用域时通过异常传播取消父)。
  • runBlocking:阻塞当前线程直到协程体完成,主要用于主函数和测试。

11. 可取消的挂起实现模式(示例)

11.1 基本 suspendCoroutine(不可自动响应取消)

kotlin
suspend fun awaitCallback(): String = suspendCoroutine { cont ->
  val callback = object : Callback {
    override fun onResult(result: String) {
      cont.resume(result)
    }
    override fun onError(t: Throwable) {
      cont.resumeWithException(t)
    }
  }
  register(callback)
  // 如果协程被取消,这里没有自动清理注册;需要额外处理
}

11.2 推荐:suspendCancellableCoroutine(带取消钩子)

kotlin
suspend fun awaitCancellable(): String = suspendCancellableCoroutine { cont ->
  val call = createCancelableCall()
  call.enqueue { result -> cont.resume(result) }
  cont.invokeOnCancellation { cause ->
    call.cancel() // 在 Job 取消时取消底层请求
  }
}

12. 协程状态机 & 内部 Result 流程(更底层)

  • Continuation.resumeWith(result) 接收 Result<T>(要么成功值要么异常)。

  • 恢复一般流程:

    1. 调用 continuation.intercepted()(由 ContinuationInterceptor 可替换),得到可能被包装的 Continuation
    2. 调度(dispatch)该 Continuation 的执行(或直接在当前线程执行,取决于 dispatcher)。
    3. 执行恢复代码块(状态机会根据保存的状态跳转到对应分支)。
  • COROUTINE_SUSPENDED 是内部标记,表示当前 suspend 还未结束,调用者应返回该标记给调用方(编译器在生成代码时使用)。


13. 性能和轻量级的原因

  • 协程对象比线程轻很多:创建、切换、内存/上下文开销远小于 OS 线程(协程只是对象与少量堆栈状态)。
  • 切换协程并不是线程上下文切换(内核切换),而是把 Runnable 入队并在某线程上执行恢复逻辑,代价低。

14. 重要实践与坑(Checklist)

  1. 记住取消是协作式:在自定义阻塞或长时间计算中要显式检查 isActive 或使用 yield()
  2. 不要把阻塞式 IO 放在 Default:把阻塞IO放 Dispatchers.IO/自定义池。
  3. 在 finally 中做挂起清理要用 NonCancellable
  4. 异常处理try/catch 保持在 launch block 内;async 的异常需要 await() 才会抛出。
  5. 使用 suspendCancellableCoroutine 构建可取消挂起,并在 invokeOnCancellation 做清理。
  6. 避免使用 GlobalScope 除非你真的想要全局生命周期
  7. 测试:使用 runTestTestDispatcher 控制时间、调度与取消。

15. 常见高级话题(简要导览)

  • Select:类似 select/select!,在多个挂起点之间选择第一个完成的(用于竞态)。
  • Channels / Actors:基于协程的并发通信原语(类似 Go 的 channel)。
  • Flow:基于协程的冷流,支持背压、操作符、终端收集(与 Rx 的 Observable 类比)。
  • 协程调试探针(Debug probes):可帮助链路追踪与泄露检测(DebugProbes.install 等工具)。
  • Cancellation propagation 优化CoroutineStart.LAZYSupervisorJobCoroutineScope 的不同策略。

16. 例子 — 从头到尾一个完整场景

模拟:发起网络请求,支持取消、超时、并在 finally 中执行清理(使用协程 API):

kotlin
suspend fun fetchDataWithTimeout(): String = coroutineScope {
  val job = launch {
    // 监控任务(可选)
  }
  try {
    withTimeout(5_000) {
      suspendCancellableCoroutine<String> { cont ->
        val call = httpClient.newCall(request)
        call.enqueue(object : Callback {
          override fun onResponse(call: Call, response: Response) {
            cont.resume(response.body!!.string())
          }
          override fun onFailure(call: Call, e: IOException) {
            cont.resumeWithException(e)
          }
        })
        cont.invokeOnCancellation {
          call.cancel() // 被取消时取消底层请求
        }
      }
    }
  } finally {
    withContext(NonCancellable) {
      // 清理日志、指标上报等
    }
  }
}

17. 总结(要点回顾)

  • Kotlin 协程靠编译器将 suspend 转为 Continuation 状态机,从而实现“看似同步、实为异步且不阻塞线程”的效果。
  • 挂起点保存状态并返回,恢复通过 Continuation.resumeWith 执行;Dispatcher 决定恢复在哪个线程执行。
  • 取消是协作式:需要使用 suspendCancellableCoroutineisActiveyield() 等来显式支持取消,并在 invokeOnCancellation 做资源释放。
  • 结构化并发(coroutineScopeSupervisorJob)是 Kotlin 协程的核心设计,保证可控的生命周期与异常语义。
  • 异常处理有别于 Rx:流式 onError 与协程的 try/catchCancellationExceptionCoroutineExceptionHandler 之间需要区别与配合。

随便写写的,喜欢就好。 使用VitePress构建