Coroutines — 关键概念:Kotlin中的Job与Dispatchers

作者: IT Sectr 发布日期: 2026-03-16 阅读时间: 8 分钟

Coroutines(协程)是Kotlin中用于异步编程的轻量级线程,可通过kotlinx.coroutines库获得。根据JetBrains Kotlin Documentation, 2026Coroutines允许在不阻塞线程的情况下暂停函数的执行,这与传统的Thread不同。协程在有限的线程池上运行,使其比原生线程轻量数千倍。Kotlin Coroutines与Android Jetpack、Retrofit、Room以及Android生态系统的其他流行库完全集成。

要点

  • Coroutines — Kotlin的轻量级线程,用于无阻塞异步代码
  • Suspend函数 — 能够在不阻塞线程的情况下暂停和恢复的函数
  • Dispatcher决定执行协程的线程池
  • Job — 协程的描述符,支持取消和状态跟踪
  • CoroutineScope管理协程的生命周期及其在完成时的取消

什么是Kotlin协程

Coroutines(协程)是Kotlin中的异步编程机制,在kotlinx.coroutines库中实现。与操作系统线程不同,协程不绑定到特定线程:它们可以在一个线程上暂停并在另一个线程上恢复。一个线程可以执行数千个协程,在它们之间以最小的开销进行切换。

协程在Kotlin 1.3(2018)中作为实验性功能出现,并在Kotlin 1.5(2021)中稳定。Coroutines与async/await类似地解决了回调地狱问题,但提供了更丰富的API:通道(Channel)、Flow、Job层次结构中的异常处理以及直接与Android Lifecycle的集成。

根据JetBrains(2025)的数据,每个协程大约消耗100字节的内存,而原生线程则需要1+ MB。这使得可以在一个应用中运行数百万个协程而不会出现OutOfMemoryError的风险。正是协程的轻量性使其成为Android中异步编程的首选工具。

协程在底层如何工作

每个Kotlin协程通过Continuation Passing Style(CPS)编译为状态机。编译器为每个suspend函数添加一个隐藏的Continuation参数。Continuation包含恢复点和所有局部变量。当协程暂停时,运行时保存Continuation,当恢复时,在Dispatcher池中的任何可用线程上恢复它。

Suspend函数:暂停与恢复

suspend — Kotlin的关键字,将函数标记为可暂停。这样的函数只能从另一个suspend函数或协程中调用。在suspend函数内部,可以以任意顺序调用其他suspend函数,每个调用点都是潜在的暂停点。

机制很简单:当一个suspend函数调用另一个suspend函数时,它会在该点暂停,释放线程。被调用的函数完成后,运行时从保存的位置继续执行。这称为cooperative cancellation — 没有任何线程被阻塞。

  • 暂停 — 协程释放线程而不阻塞它
  • 恢复 — 协程从暂停的同一位置继续
  • 线程 — 协程可以在线程A上暂停并在线程B上恢复
  • 异常 — 通过try/catch处理,与同步代码相同

重要:suspend函数默认不是异步的。如果不使用launch或async,执行顺序保持顺序。suspend仅允许函数在不阻塞线程的情况下被暂停,并成为协程上下文的一部分。Continuation Passing Style是一种编译模型,其中每个suspend函数接收一个隐藏的Continuation回调,编译器生成一个状态机来管理暂停和恢复。

CoroutineScope与结构化并发

CoroutineScope是定义协程生命周期的上下文。所有协程必须在scope内部启动。当scope被取消时(例如,Activity结束时),其所有子协程会自动取消。这可以防止后台任务泄漏。Android Jetpack为每个组件提供现成的scope:viewModelScope用于ViewModel,lifecycleScope用于Activity和Fragment,它们在相应组件销毁时自动取消。

结构化并发 — 保证协程在其所有子协程完成之前不会完成的原则。Job层次结构形成一棵树:根协程创建parent job,子协程创建child jobs。Parent job的取消会传播到所有子项。结构化并发是协程与线程之间的根本区别。

Scope使用位置取消方式
GlobalScope仅用于守护任务不会自动取消
viewModelScopeAndroid ViewModelViewModel清理时
lifecycleScopeAndroid Activity/FragmentLifecycle销毁时
coroutineScope在suspend函数内部父任务取消时

用于错误处理的SupervisorJob

普通的Job在某个子协程出错时会取消所有兄弟协程。SupervisorJob是一个例外:一个子协程的失败不会影响其他子协程。当多个独立任务并行执行且其中一个可能失败而不需要取消其他任务时,这很重要。

Dispatchers与协程构建器

Dispatchers决定协程在哪些线程上执行。Dispatchers.Main — Android的主UI线程。Dispatchers.IO — 用于阻塞操作(网络、磁盘)的池。Dispatchers.Default — 用于CPU密集型任务。Dispatchers.Unconfined — 在当前线程上启动,但不保证停留在该线程上。选择正确的Dispatcher对性能至关重要:Default上的IO任务会阻塞计算池,而IO上的CPU任务会创建不必要的线程。

withContext — 用于在协程内部切换Dispatcher的函数。例如,执行JSON解析的suspend函数可以在计算期间切换到Dispatchers.Default,并返回到Dispatchers.Main以更新UI。withContext是Android开发中最常用的构建器。

三种主要的协程构建器

launch — 启动协程,返回Job,不返回结果(fire-and-forget)。async — 启动协程,返回Deferred,可以通过await从中获取结果。runBlocking — 阻塞当前线程以执行协程(仅用于测试和main函数)。构建器的选择取决于场景:launch适用于事件和更新,async适用于有结果的任务,runBlocking仅适用于测试或入口点。

Kotlin中协程的代码示例

让我们来看三个实际场景:使用launch的基本协程、使用async的并行调用以及使用SupervisorJob的错误处理。

使用launch启动协程

viewModelScope.launch在ViewModel上下文中启动协程。当ViewModel清理时,协程会自动取消。

kotlin
class ProfileViewModel : ViewModel() {
    fun loadUser() {
        viewModelScope.launch(Dispatchers.IO) {
            val user = api.fetchUser()
            withContext(Dispatchers.Main) {
                showUser(user)
            }
        }
    }
}

使用async进行并行请求

coroutineScope与async并行启动三个请求。结果通过.await()收集。如果任何一个请求失败,所有请求都会被取消。

kotlin
suspend fun loadDashboard(): Dashboard = coroutineScope {
    val user = async { api.fetchUser() }
    val posts = async { api.fetchPosts() }
    val stats = async { api.fetchStats() }
    Dashboard(user.await(), posts.await(), stats.await())
}

使用SupervisorJob进行错误处理

SupervisorJob允许每个协程独立完成。一个请求中的错误不会取消其他请求。

kotlin
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
    try { api.fetchUsers() } catch (e: Exception) { log(e) }
}
scope.launch {
    try { api.fetchPosts() } catch (e: Exception) { log(e) }
}

协程与线程:比较与场景

线程(Thread)是操作系统的原语。每个线程都有自己的栈(约1 MB),并且需要系统调用来创建和切换。协程是语言原语,不绑定到操作系统。它们使用Continuation来保存状态,并在运行时级别切换而无需系统调用。

  • 内存 — 线程约1 MB,协程约100字节。相差10 000倍
  • 创建 — 线程约1微秒系统调用,协程约0.01微秒(JVM级别)
  • 切换 — 线程约0.1微秒(系统调用),协程约0.001微秒(continuation)
  • 最大数量 — 数千个线程 vs 数百万个协程(设备上)
  • 取消 — 线程无法从外部取消(已弃用的Thread.stop),协程可以通过Job.cancel()取消

根据Google(2025)的数据,使用协程代替线程可将Android应用中后台任务的内存消耗降低90–95%。所有现代Android库(Retrofit、Room、WorkManager)都通过suspend函数内置了对协程的支持。Ktor(JetBrains的HTTP客户端框架)也完全构建在协程之上,为每个请求提供suspend函数,无需回调API。Room通过DAO中的suspend函数支持协程,允许在不阻塞主线程的情况下执行数据库查询。

何时使用线程代替协程

线程仍然需要通过JNI的原生代码、无时间限制的阻塞CPU密集型调用(视频渲染、模拟)以及与C库集成时使用。对于其他所有情况 — 使用协程。

常见问题

协程与线程有什么区别?

协程 — 一种可暂停的工作单元,在现有线程上执行。线程是具有自己栈的系统资源。协程比线程轻量数千倍,并且在暂停时不会阻塞资源。

什么是Dispatchers.IO,它与Default有何不同?

Dispatchers.IO用于阻塞I/O操作(网络、文件),可以在必要时创建新线程。Dispatchers.Default具有固定大小的池(CPU核心数),用于CPU密集型计算。

如何取消正在运行的协程?

Job.cancel()取消协程及其所有子项。要检查协程内部的取消状态,请使用ensureActive() — 如果协程已取消,它会抛出CancellationException。

协程可以与RxJava一起使用吗?

可以 — 通过kotlinx-coroutines-rx3库。它提供awaitSingle、awaitFirst等函数,用于将Observable/Single转换为suspend函数,并通过flowable进行反向转换。

协程中的Flow是什么?

Flow — 一种冷异步数据流,相当于协程的RxJava Observable。Flow按顺序发出值,并以异常或成功结束。支持map、filter、catch等操作符。

总结

  • Coroutines — Kotlin的轻量级线程,通过Continuation Passing Style实现无阻塞暂停
  • suspend — 用于标记可暂停函数的关键字
  • Dispatchers管理线程池:Main、IO、Default分别对应
  • CoroutineScope将协程的生命周期绑定到组件(Activity、ViewModel)
  • launch启动无结果的协程,async/await — 有结果
  • 结构化并发保证子协程的层次化取消
  • 协程与线程 — 协程轻量10 000倍,是Android的标准

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读