Kotlin 语言参考文档 中文版 Help

协程的基本概念

要创建能够同时执行多个任务的应用程序, 也就是称为并发的概念, Kotlin 使用 协程(Coroutine). 协程是一种可挂起的计算, 让你能够以清晰的顺序风格, 编写并发代码. 协程可以与其他协程并发(Concurrent)运行, 也可以并行(Parallel)运行.

在 JVM 和 Kotlin/Native 中, 所有的并发代码, 例如协程, 都运行在由操作系统管理的 线程(Thread) 上. 协程可以挂起执行, 而不是阻塞线程. 这样, 一个协程可以在等待某些数据到达时挂起, 而另一个协程可以在同一个线程上运行, 从而确保有效利用资源.

比较并行和并发线程

有关协程与线程的区别, 详情请参见 比较协程与 JVM 线程.

挂起函数

协程最基本的构建块是 挂起函数(Suspending Function). 它允许一个正在运行的操作暂停, 并在之后恢复, 而不影响代码的结构.

要声明挂起函数, 请使用 suspend 关键字:

suspend fun greet() { println("Hello world from a suspending function") }

挂起函数只能从另一个挂起函数中调用. 要在 Kotlin 应用程序的入口点调用挂起函数, 请用 suspend 关键字标记 main() 函数:

suspend fun main() { showUserInfo() } suspend fun showUserInfo() { println("Loading user...") greet() println("User: John Smith") } suspend fun greet() { println("Hello world from a suspending function") }

这个示例还没有使用并发, 但通过用 suspend 关键字标记函数, 你允许它们调用其他挂起函数, 并在其内部运行并发代码.

虽然 suspend 关键字是 Kotlin 核心语言的一部分, 但协程的大多数功能, 都通过 kotlinx.coroutines 库提供.

向你的项目添加 kotlinx.coroutines 库

要在你的项目中引入 kotlinx.coroutines 库, 请根据你的构建工具, 添加相应的依赖项配置:

// build.gradle.kts repositories { mavenCentral() } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") }
// build.gradle repositories { mavenCentral() } dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0' }
<!-- pom.xml --> <project> <dependencies> <dependency> <groupId>org.jetbrains.kotlinx</groupId> <artifactId>kotlinx-coroutines-core</artifactId> <version>1.11.0</version> </dependency> </dependencies> ... </project>

创建你的第一个协程

要在 Kotlin 中创建协程, 你需要以下内容:

我们来看看一个示例, 它在多线程环境中使用多个协程:

  1. 导入 kotlinx.coroutines 库:

    import kotlinx.coroutines.*
  2. 用 suspend 关键字标记可以暂停和恢复的函数:

    suspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") } suspend fun main() {}

  3. 添加 delay() 函数来模拟挂起任务, 例如获取数据或写入数据库:

    suspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") delay(1000L) }
  4. 使用 withContext(Dispatchers.Default), 为运行在共享线程池上的多线程并发代码定义入口点:

    suspend fun main() { withContext(Dispatchers.Default) { // 在这里添加协程构建器 } }

  5. 使用 协程构建器函数, 例如 CoroutineScope.launch(), 启动协程:

    suspend fun main() { withContext(Dispatchers.Default) { // this: CoroutineScope // 在作用域内, 使用 CoroutineScope.launch() 启动协程 this.launch { greet() } println("The withContext() on the thread: ${Thread.currentThread().name}") } }
  6. 组合这些部分, 在共享的线程池上同时运行多个协程:

    // 导入协程库 import kotlinx.coroutines.* // 导入 kotlin.time.Duration, 表达秒单位的时间段 import kotlin.time.Duration.Companion.seconds // 定义一个挂起函数 suspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") // 挂起 1 秒, 并释放线程 delay(1.seconds) // delay() 函数在这里模拟一个挂起的 API 调用 // 你可以在这里添加挂起的 API 调用, 例如网络请求 } suspend fun main() { // 在共享的线程池上, 运行这个代码块之内的代码 withContext(Dispatchers.Default) { // this: CoroutineScope this.launch() { greet() } // 启动另一个协程 this.launch() { println("The CoroutineScope.launch() on the thread: ${Thread.currentThread().name}") delay(1.seconds) // delay 函数在这里模拟一个挂起的 API 调用 // 你可以在这里添加挂起的 API 调用, 例如网络请求 } println("The withContext() on the thread: ${Thread.currentThread().name}") } }

试试多次运行这个示例. 你可能会注意到, 每次运行程序时的输出顺序和线程名称可能都会改变, 因为操作系统决定线程何时运行.

协程作用域与结构化并发

当你在应用程序中运行很多协程时, 你需要一种方式, 将它们作为群组来管理. Kotlin 协程依赖一种称为 结构化并发(Structured Concurrency) 的原则, 提供这种结构.

根据这个原则, 协程组成一个父/子任务的树形层级结构, 具有相互关联的生命周期. 协程的生命周期是指, 从创建到完成, 失败或取消的一系列状态.

父协程在完成之前会等待其子协程完成. 如果父协程失败或被取消, 它的所有子协程也会被递归的取消. 以这种方式保持协程的连接, 使得取消和错误处理变得能够预测, 而且安全.

为了维持结构化并发, 新协程只能在 CoroutineScope 中启动, CoroutineScope 会定义并管理它们的生命周期. CoroutineScope 包含 协程上下文(Coroutine Context), 它定义了派发器和其他执行属性. 当你在另一个协程内部启动一个协程时, 它会自动成为其父作用域的子协程.

在 CoroutineScope 上调用 协程构建器函数, 例如 CoroutineScope.launch()), 会启动与这个作用域关联的协程的子协程. 在构建器的代码块内, 接收者 是嵌套的 CoroutineScope, 因此在这里启动的任何协程都成为它的子协程.

使用 coroutineScope() 函数创建协程作用域

要使用当前协程上下文创建新的协程作用域, 请使用 coroutineScope() 函数. 这个函数创建一个协程子树的根协程. 它是在代码块内启动的协程的直接父协程, 以及这些协程启动的任何协程的间接父协程. coroutineScope() 执行挂起代码块, 并等待代码块, 以及其中启动的任何协程执行完成.

下面是一个示例:

// 导入 kotlin.time.Duration, 表达秒单位的时间段 import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* // 如果协程上下文没有指定派发器, // CoroutineScope.launch() 会使用 Dispatchers.Default //sampleStart suspend fun main() { // 协程子树的根协程 coroutineScope { // this: CoroutineScope this.launch { this.launch { delay(2.seconds) println("Child of the enclosing coroutine completed") } println("Child coroutine 1 completed") } this.launch { delay(1.seconds) println("Child coroutine 2 completed") } } // 只有 coroutineScope 中的所有子协程执行完成后, 下面的代码才会运行 println("Coroutine scope completed") } //sampleEnd

由于这个示例中没有指定 派发器, coroutineScope() 代码块中的 CoroutineScope.launch() 构建器函数会继承当前上下文. 如果这个上下文没有指定派发器, CoroutineScope.launch() 会使用 Dispatchers.Default, 它在共享线程池上运行.

从协程作用域中提取协程构建器

在某些情况下, 你可能希望将协程构建器的调用, 例如 CoroutineScope.launch(), 提取到单独的函数中.

请看以下示例:

suspend fun main() { coroutineScope { // this: CoroutineScope // 调用 CoroutineScope.launch(), 其中 CoroutineScope 是接收者 this.launch { println("1") } this.launch { println("2") } } }

coroutineScope() 函数接受一个带有 CoroutineScope 接收者的 Lambda 表达式. 在这个 Lambda 表达式内部, 隐含的接收者是 CoroutineScope, 因此构建器函数, 例如 CoroutineScope.launch() 和 CoroutineScope.async(), 会解析为这个接收者上的 扩展函数.

要将协程构建器提取到另一个函数中, 这个函数必须声明 CoroutineScope 接收者, 否则会发生编译错误:

import kotlinx.coroutines.* //sampleStart suspend fun main() { coroutineScope { launchAll() } } fun CoroutineScope.launchAll() { // this: CoroutineScope // 在 CoroutineScope 上调用 .launch() this.launch { println("1") } this.launch { println("2") } } //sampleEnd /* -- 不将 CoroutineScope 声明为接收者, 调用 launch 会导致编译错误 -- fun launchAll() { // 编译错误: this is not defined this.launch { println("1") } this.launch { println("2") } } */

在这个示例中, launchAll() 函数不需要使用 suspend 关键字, 因为它只在当前 CoroutineScope 中启动协程, 然后立即返回. 只有当函数返回之前需要暂停并恢复执行时, 才应该将函数标记为 suspend.

协程构建器函数

协程构建器函数是一种接受 suspend Lambda 表达式 的函数, 这个 Lambda 表达式定义了要运行的协程. 以下是一些示例:

协程构建器函数需要在一个 CoroutineScope 之内运行. 这个作用域可以是现有的作用域, 也可以是使用辅助函数, 例如 coroutineScope(), runBlocking() 或 withContext(), 创建的作用域. 每个构建器定义了协程如何启动, 以及你如何与它的结果交互.

CoroutineScope.launch()

CoroutineScope.launch() 协程构建器函数是 CoroutineScope 上的扩展函数. 它在现有的 协程作用域 之内, 启动一个新协程, 而不阻塞作用域内的其它协程.

当不需要结果, 或者不想等待结果时, 可以使用 CoroutineScope.launch(), 与其他工作并行运行任务:

// 导入 kotlin.time.Duration, 表达毫秒单位的时间段 import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.* suspend fun main() { withContext(Dispatchers.Default) { performBackgroundWork() } } //sampleStart suspend fun performBackgroundWork() = coroutineScope { // this: CoroutineScope // 启动一个协程, 运行时不阻塞作用域 this.launch { // 挂起, 模拟后台工作 delay(100.milliseconds) println("Sending notification in background") } // 当前一个协程挂起时, 主协程继续执行 println("Scope continues") } //sampleEnd

运行这个示例后, 你可以看到 main() 函数不会被 CoroutineScope.launch() 阻塞, 当协程在后台工作时, 会继续运行其他代码.

CoroutineScope.async()

CoroutineScope.async() 协程构建器函数是 CoroutineScope 上的扩展函数. 它在现有 协程作用域 之内, 启动一个并发计算, 并返回一个 Deferred 句柄, 表示最终的结果. 使用 .await() 函数挂起代码, 直到得到结果:

// 导入 kotlin.time.Duration, 表达毫秒单位的时间段 import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.* //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // this: CoroutineScope // 开始下载第一页 val firstPage = this.async { delay(50.milliseconds) "First page" } // 开始并行的下载第二页 val secondPage = this.async { delay(100.milliseconds) "Second page" } // 等待两个结果, 并比较它们 val pagesAreEqual = firstPage.await() == secondPage.await() println("Pages are equal: $pagesAreEqual") } //sampleEnd

runBlocking()

runBlocking() 协程构建器函数创建一个协程作用域, 并阻塞当前 线程, 直到在这个作用域中启动的协程执行结束.

只有在没有其他方式可以从非挂起代码调用挂起代码时, 才使用 runBlocking():

import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.* // 一个你无法修改的第三方接口 interface Repository { fun readItem(): Int } object MyRepository : Repository { override fun readItem(): Int { // 桥接到一个挂起函数 return runBlocking { myReadItem() } } } suspend fun myReadItem(): Int { delay(100.milliseconds) return 4 }

协程派发器(Dispatcher)

协程派发器(Dispatcher) 控制使用哪个线程或线程池来执行协程. 协程并不一定绑定到单个线程. 根据派发器不同, 协程可以在一个线程上暂停, 在另一个线程上恢复. 这样你就可以同时运行多个协程, 而不需要为每个协程分配单独的线程.

派发器与 协程作用域 一起工作, 定义协程何时运行, 以及在哪里运行. 协程作用域控制协程的生命周期, 派发器控制用于执行的线程.

kotlinx.coroutines 库包含各种不同的派发器, 适用于不同的使用场景. 例如, Dispatchers.Default 在共享线程池上运行协程, 独立于主线程之外, 在后台执行工作. 因此它成为 CPU 密集型操作, 例如数据处理, 的理想选择.

要为协程构建器, 例如 CoroutineScope.launch(), 指定派发器, 请将它作为参数:

suspend fun runWithDispatcher() = coroutineScope { // this: CoroutineScope this.launch(Dispatchers.Default) { println("Running on ${Thread.currentThread().name}") } }

或者, 你可以使用 withContext() 代码块, 在指定的派发器上运行其中的所有代码:

// 导入 kotlin.time.Duration, 表达毫秒单位的时间段 import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.* //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // this: CoroutineScope println("Running withContext block on ${Thread.currentThread().name}") val one = this.async { println("First calculation starting on ${Thread.currentThread().name}") val sum = (1L..500_000L).sum() delay(200L) println("First calculation done on ${Thread.currentThread().name}") sum } val two = this.async { println("Second calculation starting on ${Thread.currentThread().name}") val sum = (500_001L..1_000_000L).sum() println("Second calculation done on ${Thread.currentThread().name}") sum } // 等待两个计算结果, 并打印总和 println("Combined total: ${one.await() + two.await()}") } //sampleEnd

关于协程派发器及其使用, 包括其他派发器, 例如 Dispatchers.IO 和 Dispatchers.Main, 详情请参见 协程上下文与派发器.

比较协程与 JVM 线程

尽管协程与 JVM 上的线程类似, 是可挂起计算, 能够并发的运行代码, 但它们的底层工作方式是不同的.

线程 由操作系统管理. 线程可以在多个 CPU 核心上并行运行任务, 是 JVM 上并发的标准方式. 当你创建一个线程时, 操作系统会为它的栈分配内存, 并使用内核在线程之间切换. 这使得线程功能强大, 但也消耗大量资源. 每个线程通常需要几兆字节内存, JVM 通常只能同时处理几千个线程.

另一方面, 协程不绑定到特定的线程. 它可以在一个线程上挂起, 在另一个线程上恢复, 因此多个协程可以共用同一个线程池. 当协程挂起时, 线程不会被阻塞, 可以运行其他任务. 这使得协程比线程轻量很多, 能够在一个进程中运行几百万个协程, 而不会耗尽系统资源.

比较协程与线程

我们来看一个示例, 其中 50,000 个协程各自等待 5 秒, 然后打印一个点号 (.):

import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* suspend fun main() { withContext(Dispatchers.Default) { // 启动 50,000 个协程, 每个协程等待 5 秒, 然后打印一个点号 printPeriods() } } //sampleStart suspend fun printPeriods() = coroutineScope { // this: CoroutineScope // 启动 50,000 个协程, 每个协程等待 5 秒, 然后打印一个点号 repeat(50_000) { this.launch { delay(5.seconds) print(".") } } } //sampleEnd

现在让我们看看使用 JVM 线程的相同示例:

import kotlin.concurrent.thread fun main() { repeat(50_000) { thread { Thread.sleep(5000L) print(".") } } }

运行这个版本会使用更多内存, 因为每个线程都需要自己的内存栈. 对于 50,000 个线程, 内存消耗可能高达 100 GB, 而同等数量的协程只需要大约 500 MB.

根据你的操作系统, JDK 版本和设置, JVM 线程版本可能会抛出内存不足错误, 或者放慢线程创建速度, 以避免同时运行过多线程.

下一步做什么

2026/09/29