位置:首页 > Kotlin > Kotlin 协程官方框架速读:启动模式、调度器与取消机制一次看懂

Kotlin 协程官方框架速读:启动模式、调度器与取消机制一次看懂

时间:2026-08-22  |  作者:实验室老王  |  阅读:0

目录

  1. kotlinx.coroutines 到底提供了什么
  2. 四种启动模式,差别到底在哪
  3. 官方预置的四个调度器怎么选
  4. 没有挂起点时,协程为什么对取消没反应
  5. 超时取消:给单个任务设更短的生命期
  6. 理解官方框架,先抓住这三条主线

前言

很多人学 Kotlin 协程时,容易先记 API,再被启动模式、调度器和取消机制绕晕。与其按章节顺序硬啃,不如先把官方框架 kotlinx.coroutines 的核心能力串起来:它负责什么、协程怎么启动、在哪执行、什么时候能被取消。看完这篇,你至少能对几类常见问题有清晰判断:该选哪种启动模式、Dispatchers 怎么分工、遇到无挂起点代码时如何让取消真正生效。

很多人学 Kotlin 协程时,容易先记 API,再被启动模式、调度器和取消机制绕晕。与其按章节顺序硬啃,不如先把官方框架 kotlinx.coroutines 的核心能力串起来:它负责什么、协程怎么启动、在哪执行、什么时候能被取消。看完这篇,你至少能对几类常见问题有清晰判断:该选哪种启动模式、Dispatchers 怎么分工、遇到无挂起点代码时如何让取消真正生效。

kotlinx.coroutines 到底提供了什么

kotlinx.coroutines 是 Kotlin 协程的官方框架,它独立于标准库之外,定位是面向生产环境的异步编程支持库。平时我们说“用 Kotlin 协程”,很多关键能力其实都来自这个框架,而不是语言本身。

官方框架的主要组成

从能力划分来看,它大致可以分为几块:

  • core:框架核心逻辑,包含协程创建、调度、取消、channelflow 等。
  • ui:面向各平台 UI 的支持,包括 Android、JavaFX、Swing,对应提供 UI 调度器和相关逻辑。
  • reactive 相关:提供与响应式编程框架的衔接支持,例如 reactivereactorrx2
  • integration:处理与其他异步或平台能力的集成,例如 jdk8guavaslf4jplay-services

如果把它理解成“协程运行时工具箱”会更直观:核心模块解决协程本身怎么跑,UI 模块解决界面线程怎么切,集成模块解决你怎么把协程接到现有生态里。

四种启动模式,差别到底在哪

协程启动模式一共有四种:DEFAULTLAZYATOMICUNDISPATCHED。名字不难记,真正容易混淆的是“立即调度”和“立即执行”不是一回事。

先分清:立即调度不等于立即执行

所谓“立即调度”,是调度器马上收到了执行指令;但它具体何时执行、在哪个线程执行,还要看调度器当时的安排。也就是说,从“收到调度请求”到“代码真正开始运行”之间,通常还隔着一个过程。

而“立即执行”则更直接:协程创建后,当前调用栈里就开始跑协程代码,直到遇到第一个真正的挂起点。

把这层区别想清楚,下面四种模式就容易判断了。

DEFAULT:立刻进入调度,但未必立刻执行

  • 会立即根据上下文调度协程执行。
  • 如果在真正执行前协程就被取消,它会直接进入取消响应状态。

所以 DEFAULT 的重点不是“马上跑起来”,而是“马上提交给调度器”。这也意味着它有可能还没来得及执行,就已经被取消了。

LAZY:等你真正需要时再启动

  • 只有在协程被需要时才会开始调度。
  • 常见触发方式包括主动调用 startjoinawait
  • 如果调度前就被取消,协程会直接进入异常结束状态。

这类模式适合“先声明,后决定是否执行”的场景,但也要注意:它不是不占逻辑成本,而是把启动时机延后了。

ATOMIC:开始后,到第一个挂起点前不响应取消

  • 协程创建后会立即开始调度。
  • 在执行到第一个挂起点之前,不响应取消。

可以把它理解成:调度和开始执行之间被做成了原子操作。正因为如此,ATOMIC 能保证协程一定会先跑起来,而不是在执行前就被取消掉。

UNDISPATCHED:先在当前调用栈直接执行

  • 协程创建后立即在当前函数调用栈中执行。
  • 一直执行到遇到第一个真正的挂起点为止。

这和 ATOMIC 的相似点在于,两者都能保证协程一定执行;区别在于第一个挂起点之前运行的位置不同。

  • UNDISPATCHED:先运行在创建协程时所在的线程。
  • ATOMIC:会调度到指定调度器对应的线程上执行。

四种模式可以直接记住这几个结论

  • DEFAULT 虽然是立即调度,但仍可能在执行前被取消。
  • UNDISPATCHED 是立即执行,因此协程一定会执行。
  • ATOMIC 虽然也是立即调度,但调度与执行的开始具备原子性,因此协程也一定会执行。
  • UNDISPATCHEDATOMIC 都能保证先执行,但前者先跑在当前线程,后者先跑在目标调度器线程。

官方预置的四个调度器怎么选

官方预置了 4 个调度器,可以通过 Dispatchers 对象访问。多数项目里的线程分工,基本都绕不开它们。

Dispatchers.Default:偏计算型任务

Default 是默认调度器,适合处理后台计算任务,属于 CPU 密集型任务调度器。比如数据转换、排序、复杂计算这类不依赖磁盘或网络等待的工作,更适合放这里。

Dispatchers.IO:偏 IO 型任务

IO 适合执行 IO 相关操作,属于 IO 密集型任务调度器。文件读写、数据库访问、网络请求这类有明显等待时间的任务,通常交给它更合适。

Dispatchers.Main:面向 UI 线程

Main 是 UI 调度器,会根据平台不同初始化为对应 UI 线程的调度器。比如在 Android 中,它通常就是主线程调度器,适合更新界面、响应用户交互。

Dispatchers.Unconfined:不绑定特定线程

Unconfined 可以理解成“不强制要求协程运行在某个固定线程”。它比较特殊,适合对线程上下文要求不强、且你明确知道行为边界的场景;如果只是普通业务代码,通常还是优先选语义更明确的 DefaultIOMain

没有挂起点时,协程为什么对取消没反应

协程的取消通常发生在挂起点上。通过 suspendCancellableCoroutine,挂起函数可以感知取消状态,因此很多异步任务在设计时,天然会把“取消响应点”放在挂起处。

问题在于:如果一段代码里根本没有挂起点,取消就不一定能及时生效。

典型例子:InputStream.copyTo

标准库里的扩展函数 InputStream.copyTo 使用的是 Java BIO。它本身就是一个同步循环:

public fun InputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
    var bytesCopied: Long = 0
    val buffer = ByteArray(bufferSize)
    var bytes = read(buffer)
    while (bytes >= 0) {
        out.write(buffer, 0, bytes) 
        bytesCopied += bytes
        bytes = read(buffer)
    }
    return bytesCopied
}

把这段代码放进协程里执行后,你会发现:即使外部取消了协程,这个循环也可能照样继续跑,因为它没有挂起点,也就没有自然的取消检查时机。

可以手动补上取消检查:yield()

一种直觉做法,是在 while 循环里检查当前协程的 isActive。不过官方协程框架还提供了更直接的办法:yield()

public fun InputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
    ...
    while (bytes >= 0) {
        yield()
    ...
    }
    return bytesCopied
}

yield() 主要有两个作用:

  • 检查当前协程状态;如果已经取消,则抛出取消异常进行响应。
  • 尝试让出当前线程的执行权,给其他协程提供运行机会。

这也是处理“长循环但没有挂起点”的常见思路:你得主动插入一个能检查取消的点,否则协程取消只是状态变了,代码本身却不一定停下来。

超时取消:给单个任务设更短的生命期

在网络请求里,有些接口的超时要求会比全局配置更严格。这种时候,不必改整个系统的超时策略,可以直接对单个协程代码块使用 withTimeout

展示无挂起点循环如何通过 yield 和 withTimeout 响应取消与超时的白底信息图
取消检查与超时处理这张图聚焦最容易踩坑的部分:没有挂起点时取消为何失效,以及 yield 和。

withTimeout 的基本用法

GlobalScope.launch {
    val user = withTimeout(1000){
        ....
    }
}

这里的 1000 表示超时时间。如果 withTimeoutblock 运行超时,该代码块会被取消,并直接抛出取消异常。

不想抛异常,可以用 withTimeoutOrNull

如果不希望在超时后通过异常处理流程收尾,可以改用 withTimeoutOrNull。它在超时情况下不会抛异常,而是直接返回 null

这在“超时本身就是一种可接受结果”的场景里更好用,例如某些弱依赖接口、兜底加载逻辑或非关键附加请求。

理解官方框架,先抓住这三条主线

回头看,kotlinx.coroutines 的核心可以先抓三件事:

  • 它不仅仅是“让代码能挂起”,更是完整的生产级协程框架。
  • 启动模式决定的是协程何时开始、是否可能在执行前被取消,以及第一个挂起点前在哪个线程运行。
  • 取消能否生效,不只取决于你有没有调用 cancel,还取决于代码里有没有合适的取消检查点;没有挂起点时,往往要靠 yield() 这类手段补上。

先把这几个判断标准建立起来,再去看 Android 应用或协程框架实现细节,会顺很多。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多