Kotlin Coroutine 教程:快速入门 – wiki基地


Kotlin Coroutines 教程:快速入门与深度解析

序言:告别回调地狱与线程困境

在现代应用开发中,异步编程无处不在。无论是移动应用的 UI 刷新、网络请求、数据库操作,还是后端服务的高并发处理,都需要我们以非阻塞的方式执行耗时任务,以免阻塞主线程(如 Android UI 线程)或浪费服务器资源。

传统的异步编程方式,如多线程、回调函数、Future/Promise、RxJava 等,各有优缺点。多线程管理复杂、资源消耗大;回调函数容易导致臭名昭著的“回调地狱”(Callback Hell),代码可读性和维护性差;而 RxJava 虽然强大,但学习曲线相对陡峭,且在某些场景下可能显得过于重。

Kotlin Coroutines(协程)正是为了解决这些问题而生。它提供了一种全新的异步编程范式,让你可以用看起来像同步顺序执行的代码来编写异步逻辑。它比线程更轻量、更灵活,能够优雅地处理并发任务、简化异步流程、改善代码结构。本篇文章将带你快速入门 Kotlin Coroutines,并深入理解其核心概念和工作原理。

第一章:什么是 Coroutine?为什么需要它?

1.1 Coroutine 的本质:轻量级的线程?

简单来说,Coroutine 是一种 轻量级的并发构建块。它们不是操作系统级别的线程,而是由用户空间(即你的代码)管理的。一个操作系统线程可以同时运行成千上万个 Coroutine。

主要区别:

  • 重量级 vs 轻量级: 线程是由操作系统调度的,创建和切换的开销较大,数量受限。Coroutine 由 Coroutine 框架在用户空间调度,创建和切换开销极低,可以创建成千上万个。
  • 阻塞 vs 非阻塞: 当一个线程执行阻塞操作(如等待网络响应)时,整个线程会被挂起,无法执行其他任务。而 Coroutine 在执行阻塞操作时,只会挂起当前的 Coroutine,底层的线程可以切换去执行其他 Coroutine,从而实现非阻塞。
  • 结构化 vs 扁平化: Coroutines 支持“结构化并发”(Structured Concurrency),使得 Coroutine 的生命周期可以与特定的作用域(Scope)绑定,方便管理、取消和错误处理。

1.2 Coroutine 解决了哪些问题?

  • 简化异步代码: 将复杂的异步流程(如链式调用多个网络请求)用同步风格的代码表达,大大提高可读性和可维护性。告别层层嵌套的回调函数。
  • 避免阻塞主线程: 将耗时操作放在后台 Coroutine 中执行,而不阻塞 UI 线程或主处理线程。
  • 更高效的资源利用: 相较于线程,Coroutine 更轻量,允许你在有限的线程资源上处理大量并发任务,尤其适用于 I/O 密集型场景(如网络服务)。
  • 结构化的并发管理: Coroutine 的作用域、Job、Context 等概念提供了强大的机制来管理并发任务的生命周期、取消和异常处理,避免资源泄露和未处理的异常。

想象一下,你需要顺序执行三个异步操作:获取用户ID -> 根据ID获取用户信息 -> 根据用户信息获取订单列表。使用回调函数,这将是三层嵌套。使用 Coroutines,它看起来可以像这样:

kotlin
// 伪代码
suspend fun getUserOrders() {
val userId = getUserIdAsync() // suspend function, suspends but doesn't block
val userInfo = getUserInfoAsync(userId) // suspend function
val orders = getOrdersAsync(userInfo.id) // suspend function
updateUI(orders) // Assuming this runs on main thread
}

这段代码看起来就像同步执行一样,但底层的 Coroutine 框架会负责在 getUserIdAsyncgetUserInfoAsyncgetOrdersAsync 等函数执行 I/O 等待时,暂停当前的 Coroutine,让出底层线程给其他 Coroutine 或任务使用,等到异步操作完成后再恢复执行。这就是 Coroutine 的魔力!

第二章:快速上手 Coroutine:添加依赖与基本构建块

2.1 添加 Coroutine 依赖

要在你的 Kotlin 项目中使用 Coroutines,你需要添加相应的依赖。对于 JVM 项目(包括 Android),通常需要 kotlinx-coroutines-core。如果是在 Android 开发中需要使用 UI 相关的调度器(如 Main),还需要 kotlinx-coroutines-android

build.gradle(.kts) 文件中(通常是 app/build.gradle(.kts)):

“`gradle
// build.gradle (Groovy DSL)
dependencies {
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3”) // 或最新版本
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3”) // Android 项目需要
}

// build.gradle.kts (Kotlin DSL)
dependencies {
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3”) // 或最新版本
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3”) // Android 项目需要
}
“`
(注意:请使用 Maven Central 上 kotlinx.coroutines 的最新稳定版本)

2.2 suspend 函数:协程的基础标记

一切从 suspend 关键字开始。suspend 是 Kotlin 语言层面的一个修饰符,用于标记一个函数 可能暂停(suspend)并稍后恢复(resume)执行

kotlin
suspend fun doSomethingUseful() {
println("Doing something...")
delay(1000L) // 这是一个 suspend 函数,暂停 Coroutine 1秒
println("Done doing something!")
}

重要概念:

  • suspend 函数只能在另一个 suspend 函数或 Coroutine 构建块(如 runBlocking, launch, async)中调用。
  • suspend 关键字并不意味着函数会创建新线程。它只表示函数在执行到某个点时可以暂停当前的 Coroutine,释放底层线程,并在异步操作完成后从暂停的地方继续执行。这种暂停/恢复是由 Coroutine 框架管理的,对调用者是透明的。
  • delay(millis: Long) 是一个非常有用的 suspend 函数,它会暂停当前的 Coroutine 执行指定的时间,但不会阻塞底层线程。

2.3 runBlocking:连接阻塞世界与协程世界

通常我们的程序入口(如 main 函数)是一个普通的阻塞函数。为了在这样的函数中启动第一个 Coroutine,我们需要一个“桥梁”。runBlocking 就是这样的一个构建块。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
println(“Main starts”)

doSomethingUseful() // 在 runBlocking 内部调用 suspend 函数

println("Main ends")

}

suspend fun doSomethingUseful() {
println(“Doing something…”)
delay(1000L)
println(“Done doing something!”)
}
“`

输出:

Main starts
Doing something...
Done doing something!
Main ends

runBlocking 的特点:

  • 它会 阻塞 调用它的线程,直到其内部的 Coroutine 执行完毕。
  • 它主要用于连接非协程的阻塞代码与协程代码,例如在 main 函数、单元测试中。
  • 警告: 绝不应该 在 Android 的 UI 线程中调用 runBlocking,因为它会阻塞 UI 线程,导致应用无响应(ANR – Application Not Responding)。

2.4 launch:启动一个新的 Coroutine

launch 是最常用的 Coroutine 构建块之一。它用于启动一个新的 Coroutine,执行一段代码,然后 不等待其结果。这通常用于执行“发后即忘”(fire-and-forget)的任务,例如在后台执行计算或 I/O 操作,同时不阻塞当前流程。

launch 返回一个 Job 对象,表示这个 Coroutine 的句柄,可以用来取消 Coroutine 或等待其完成 (join())。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
println(“Main starts: ${Thread.currentThread().name}”)

val job = launch { // 启动一个新的 Coroutine
    println("Coroutine started: ${Thread.currentThread().name}")
    delay(1000L)
    println("Coroutine finished: ${Thread.currentThread().name}")
}

println("Main continues: ${Thread.currentThread().name}")
job.join() // 等待 Coroutine 完成
println("Main ends: ${Thread.currentThread().name}")

}
“`

可能的输出:

Main starts: main
Main continues: main
Coroutine started: main // 或其他线程,取决于调度器
Coroutine finished: main // 或其他线程
Main ends: main

注意: launch 默认使用的调度器(Dispatcher)取决于它所在的 CoroutineScope。在 runBlocking 内部,默认使用 runBlocking 自己的调度器(通常是一个单线程调度器或父 Coroutine 的调度器)。我们将在后续章节详细讨论调度器。

launch 的特点:

  • 启动一个新的 Coroutine,不阻塞 调用它的代码。
  • 返回一个 Job 对象,用于管理 Coroutine。
  • 适用于执行并行任务,无需返回结果的场景。

2.5 async & await:并发执行并获取结果

launch 不同,async 用于启动一个 Coroutine 并期望它返回一个结果。async 返回一个 Deferred<T> 对象,这是一个特殊的 Job,你可以调用其 await() 方法来获取 Coroutine 的计算结果。await() 是一个 suspend 函数,它会暂停当前的 Coroutine,直到 Deferred 完成并产生结果。

async 通常用于需要并行执行多个任务,并等待所有任务都完成后再继续的场景。

“`kotlin
import kotlinx.coroutines.
import kotlin.system.

suspend fun doSomethingOne(): Int {
delay(1000L) // 模拟耗时操作
println(“Done with something one”)
return 10
}

suspend fun doSomethingTwo(): Int {
delay(2000L) // 模拟更耗时操作
println(“Done with something two”)
return 20
}

fun main() = runBlocking { // this: CoroutineScope
println(“Main starts”)

val time = measureTimeMillis {
    val resultOne = async { doSomethingOne() } // 启动 Coroutine 1
    val resultTwo = async { doSomethingTwo() } // 启动 Coroutine 2

    println("Waiting for results...")
    // await 是 suspend 函数,会暂停直到对应的 async 完成
    val totalResult = resultOne.await() + resultTwo.await()
    println("Total result: $totalResult")
}

println("Completed in $time ms")
println("Main ends")

}
“`

可能的输出:

Main starts
Waiting for results...
Done with something one // 大约1秒后
Done with something two // 大约2秒后
Total result: 30
Completed in 2xxx ms // 总时间接近最长的那个 async 的时间,因为它们是并行执行的
Main ends

如果这里不是用 async 并行,而是顺序调用 doSomethingOne()doSomethingTwo(),总时间会是 1000 + 2000 = 3000ms。通过 async,我们将这两个任务并行化,总时间取决于其中最慢的任务(在这个例子中是 doSomethingTwo 的 2000ms,加上一点点 Coroutine 调度的开销)。

async 的特点:

  • 启动一个新的 Coroutine,不阻塞 调用它的代码。
  • 返回一个 Deferred<T> 对象,它是 Job 的子类,代表一个未来的结果。
  • 调用 await() 方法获取结果,await()suspend 函数。
  • 适用于需要并行执行多个任务并等待所有结果的场景。

launch vs async 总结:

  • launch:执行任务,不需要返回结果 (Job)。适用于副作用操作(如更新 UI,写入日志)。
  • async:执行任务,需要返回结果 (Deferred<T>)。适用于计算任务或需要聚合多个异步操作结果的场景。

第三章:Coroutine 上下文与调度器(Context & Dispatchers)

3.1 Coroutine Context

每个 Coroutine 都关联一个 CoroutineContext。它是一组元素的集合,这些元素定义了 Coroutine 的行为和环境。CoroutineContext 主要包括以下几个重要元素:

  • Job Coroutine 的唯一标识和生命周期管理(运行中、已取消、已完成等)。
  • CoroutineDispatcher 决定 Coroutine 在哪个线程或线程池上执行。
  • CoroutineName 用于调试的 Coroutine 名称。
  • CoroutineExceptionHandler 处理 Coroutine 中未捕获的异常。

当你使用 runBlocking, launch, async 等构建块时,可以在后面指定 CoroutineContext。如果没有指定,它会继承父 Coroutine 的 Context。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
// 继承 runBlocking 的 context
launch {
println(“Coroutine 1: I’m in ${coroutineContext[Job]} on ${Thread.currentThread().name}”)
}

// 指定一个 CoroutineName
launch(CoroutineName("MyNamedCoroutine")) {
    println("Coroutine 2: I'm in ${coroutineContext[CoroutineName]} on ${Thread.currentThread().name}")
}

delay(100) // 给点时间让上面的 Coroutine 打印输出

}
“`

可能的输出:

Coroutine 1: I'm in JobImpl{Active}@... on main // 或 runBlocking 默认线程
Coroutine 2: I'm in CoroutineName(MyNamedCoroutine) on main // 或 runBlocking 默认线程

3.2 Coroutine Dispatcher:决定执行在哪

CoroutineDispatcherCoroutineContext 中的一个关键元素,它决定了 Coroutine 在哪个线程或线程池上执行。Kotlin Coroutines 提供了几种内置的调度器:

  • Dispatchers.Default 默认调度器,用于 CPU 密集型任务。它使用一个共享的后台线程池,线程数量默认等于 CPU 核心数。
  • Dispatchers.IO 用于 I/O 密集型任务,如网络请求、文件读写、数据库操作。它使用一个更大的共享线程池,线程数量会根据需要增长,但有上限。推荐将大部分耗时的阻塞 I/O 操作切换到此调度器。
  • Dispatchers.Main (仅在支持的平台如 Android 或 JavaFX 中可用) 主线程调度器,用于与 UI 交互。所有 UI 更新都必须在此调度器上执行。
  • Dispatchers.Unconfined 不受限调度器。它在调用它的当前线程上启动 Coroutine,但 Coroutine 第一次遇到 suspend 点之后,可能会在任何其他合适的线程上恢复执行。不推荐在代码中使用,主要用于一些特殊场景或测试。

你可以通过在 launchasync 的第一个参数中指定调度器来切换 Coroutine 的执行线程:

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
// 在 Default 调度器上启动 Coroutine
launch(Dispatchers.Default) {
println(“Coroutine 1 (Default): On ${Thread.currentThread().name}”)
// CPU 密集型计算
}

// 在 IO 调度器上启动 Coroutine
launch(Dispatchers.IO) {
    println("Coroutine 2 (IO): On ${Thread.currentThread().name}")
    // 网络请求或文件读写
}

// 在 runBlocking 的上下文中(通常是 main 线程或单线程)
launch {
    println("Coroutine 3 (Inherited): On ${Thread.currentThread().name}")
    // 轻量级或非阻塞操作
}

delay(100) // 等待 Coroutine 打印输出

}
“`

可能的输出:

Coroutine 3 (Inherited): On main // 或 runBlocking 默认线程
Coroutine 1 (Default): On DefaultDispatcher-worker-1 // 或其他 worker 线程
Coroutine 2 (IO): On DefaultDispatcher-worker-2 // 或其他 worker 线程

3.3 withContext:在 Coroutine 中切换上下文

withContext 是一个非常强大的 suspend 函数,用于在 Coroutine 执行过程中 切换上下文。它会暂停当前 Coroutine,切换到指定的 Context(通常是不同的 Dispatcher)执行一段代码块,然后 切回原来的 Context,并返回代码块的结果。

这在 Android 开发中尤其有用,例如在 IO 线程执行网络请求,然后切换回 Main 线程更新 UI:

“`kotlin
import kotlinx.coroutines.*

suspend fun fetchData(): String {
// 假设当前 Coroutine 在 Dispatchers.Main
println(“Fetching data: On ${Thread.currentThread().name}”)

// 切换到 IO 调度器执行耗时操作
return withContext(Dispatchers.IO) {
    println("Simulating network request: On ${Thread.currentThread().name}")
    delay(2000L) // 模拟网络延迟
    "Data fetched successfully!"
}
// withContext 结束后,自动切回原来的调度器 (Dispatchers.Main)

}

fun main() = runBlocking {
// 模拟在主线程(或类似场景)启动
println(“Starting operation: On ${Thread.currentThread().name}”)

val result = fetchData() // 调用 suspend 函数

println("Operation finished: On ${Thread.currentThread().name}")
println("Result: $result")

}
“`

可能的输出:

Starting operation: On main
Fetching data: On main
Simulating network request: On DefaultDispatcher-worker-1 // 或 IO 线程
Operation finished: On main
Result: Data fetched successfully!

withContext 优雅地解决了线程切换问题,使得异步代码逻辑清晰,避免了复杂的回调或线程同步代码。

第四章:结构化并发(Structured Concurrency)

Kotlin Coroutines 的一个核心特性是 结构化并发。这意味着新的 Coroutine 只能在现有的 CoroutineScope 中启动。CoroutineScope 会追踪它创建的所有 Coroutine。当 Scope 被取消时,它会取消其下的所有子 Coroutine。这极大地简化了 Coroutine 的生命周期管理,避免了协程泄露。

4.1 CoroutineScope

CoroutineScope 是一个接口,定义了 coroutineContext 属性。它代表了 Coroutine 的作用域,通常与应用的某个生命周期绑定(如 Android Activity/ViewModel 的生命周期)。

你不能直接创建一个 CoroutineScope 的实例,而是通过 CoroutineScope() 工厂函数或者使用预定义的 Scope (如 Android 的 viewModelScope, lifecycleScope)。

“`kotlin
import kotlinx.coroutines.*

val applicationScope = CoroutineScope(Dispatchers.Default) // 创建一个应用级别的 Scope

fun main() = runBlocking {
val job = applicationScope.launch {
// 这个 Coroutine 运行在 applicationScope 中
println(“Task started in applicationScope”)
delay(2000)
println(“Task finished in applicationScope”)
}

delay(1000) // 等待 1 秒
println("Cancelling applicationScope...")
applicationScope.cancel() // 取消 Scope 中的所有 Coroutine
println("applicationScope cancelled.")

job.join() // 等待 job 最终结束 (由于取消,它会以 CancellationException 结束)

}
“`

可能的输出:

Task started in applicationScope
Cancelling applicationScope...
Task finished in applicationScope // 注意:如果取消得早,这行可能不会打印
applicationScope cancelled.
Exception in thread "main" kotlinx.coroutines.CancellationException: Job was cancelled... // runBlocking 会传播取消异常

(在上面的例子中,delay(2000) 是一个可取消的 suspend 函数,当 applicationScope.cancel() 被调用时,delay 会被中断,Coroutine 会抛出 CancellationException 并结束。)

4.2 Job 的层级关系

当你在一个 CoroutineScope 中使用 launchasync 创建新的 Coroutine 时,新的 Coroutine 的 Job 会成为 Scope 的 Job子 Job。取消父 Job 会导致所有子 Job 被取消。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
// this: CoroutineScope, 它的 Job 是 runBlocking 创建的 Job
val parentJob = coroutineContext[Job]
println(“Parent Job: $parentJob”)

val childJob1 = launch {
    // 这个 launch 创建的 Job 是 parentJob 的子 Job
    println("Child 1: Started")
    delay(1000)
    println("Child 1: Finished")
}

val childJob2 = launch {
    // 这个 launch 创建的 Job 也是 parentJob 的子 Job
    println("Child 2: Started")
    delay(2000)
    println("Child 2: Finished")
}

println("Children of Parent Job: ${parentJob?.children?.toList()}")

delay(500) // 等待孩子们启动
println("Cancelling Parent Job...")
parentJob?.cancel() // 取消父 Job

// 不需要显式 join 子 Job,因为父 Job 取消会取消所有子 Job,然后父 Job 才会结束
println("Waiting for Parent Job to finish...")

}
“`

可能的输出:

Parent Job: JobImpl{Active}@...
Child 1: Started
Child 2: Started
Children of Parent Job: [JobImpl{Active}@..., JobImpl{Active}@...]
Cancelling Parent Job...
Waiting for Parent Job to finish...
// 因为父 Job 被取消,子 Job 也会被取消
// 最终 runBlocking 会结束,可能抛出 CancellationException

这种父子结构是结构化并发的核心。它确保了 Coroutine 的生命周期被有效管理,避免了“孤儿”Coroutines 导致的资源泄露。

4.3 coroutineScope 与 supervisorScope

  • coroutineScope 这是一个 suspend 函数,用于创建一个 子作用域。它会等待其内部启动的所有子 Coroutine 完成。如果任何一个子 Coroutine 失败(抛出异常),coroutineScope 会取消其所有其他子 Coroutine,并将异常传播给它的父 Coroutine。这适用于一组任务“要么全部成功,要么全部失败”的场景。
  • supervisorScope 类似 coroutineScope,也用于创建一个子作用域并等待其子 Coroutine 完成。但不同的是,supervisorScope 不会 在子 Coroutine 失败时取消其他子 Coroutine。它适用于一组独立的任务,一个任务的失败不应该影响其他任务。异常只会影响失败的那个子 Coroutine,并可能需要一个 CoroutineExceptionHandler 来处理。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
println(“Using coroutineScope:”)
try {
coroutineScope { // 创建子作用域
launch {
delay(1000)
println(“coroutineScope Child 1 finished”)
}
launch {
delay(500)
println(“coroutineScope Child 2 throwing exception”)
throw RuntimeException(“Oops!”)
}
}
} catch (e: Exception) {
println(“coroutineScope caught exception: ${e.message}”)
}

println("\nUsing supervisorScope:")
val handler = CoroutineExceptionHandler { _, exception ->
    println("supervisorScope caught exception: $exception")
}
supervisorScope { // 创建子作用域
    launch(handler) {
        delay(1000)
        println("supervisorScope Child 1 finished")
    }
    launch(handler) {
        delay(500)
        println("supervisorScope Child 2 throwing exception")
        throw RuntimeException("Oops!")
    }
}

}
“`

可能的输出:

“`
Using coroutineScope:
coroutineScope Child 2 throwing exception
coroutineScope caught exception: Oops! // Child 2 失败导致 Child 1 被取消

Using supervisorScope:
supervisorScope Child 2 throwing exception
supervisorScope caught exception: java.lang.RuntimeException: Oops!
supervisorScope Child 1 finished // Child 1 不受 Child 2 失败的影响
“`

理解 coroutineScopesupervisorScope 的区别对于构建健壮的并发程序至关重要。

第五章:取消与异常处理

Coroutine 的取消和异常处理是结构化并发的重要组成部分。

5.1 Coroutine 的取消

Coroutine 的取消是 协作式 的。这意味着 Coroutine 本身必须配合取消操作才能真正停止执行。大多数 Kotlin Coroutine 库提供的 suspend 函数(如 delay, yield, withContext, withTimeout 等)都是可取消的,它们在被取消时会抛出 CancellationException

如果你在一个 Coroutine 中执行长时间运行的 非协作 计算(如一个无限循环或一个复杂的计算),并且没有在循环内部检查取消状态,那么这个 Coroutine 将无法被外部取消。

检查取消状态的方法:

  1. 使用 isActive 属性(它是 CoroutineScope 的扩展属性,通过 coroutineContext[Job] 获取)。
  2. 使用 ensureActive() 函数,如果 Coroutine 不活跃(已取消),它会抛出 CancellationException
  3. 使用 yield() 函数,它会检查取消状态,如果已取消则抛出 CancellationException,否则让出线程供其他 Coroutine 使用。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (isActive) { // 检查 isActive
try {
println(“Coroutine running… ${i++}”)
delay(100) // delay 是一个可取消的 suspend 函数
} catch (e: CancellationException) {
println(“Coroutine caught cancellation: ${e.message}”)
throw e // 重新抛出异常以传播取消
}
}
println(“Coroutine finished loop”) // 这行通常不会被打印,因为循环在取消时退出
}

delay(500) // 运行 0.5 秒
println("Cancelling coroutine...")
job.cancel() // 发送取消信号
job.join() // 等待 coroutine 完成(因取消而停止)
println("Coroutine cancelled and joined.")

}
“`

可能的输出:

Coroutine running... 0
Coroutine running... 1
Coroutine running... 2
Coroutine running... 3
Coroutine running... 4
Cancelling coroutine...
Coroutine caught cancellation: Job was cancelled...
Coroutine cancelled and joined.

while(isActive) 循环中使用 delayyield 是使计算代码可取消的常用模式。如果你的计算是纯 CPU 密集型的且没有 suspend 函数,你需要定期检查 isActive 或调用 ensureActive()

5.2 异常处理

Coroutine 中的异常处理与常规 Kotlin 代码类似,可以使用 try/catch 块。

  • 对于 launch 启动的 Coroutine: 未捕获的异常会在 Coroutine Context 中传播。如果 Coroutine Context 中有 CoroutineExceptionHandler,它会被调用。如果没有,异常会到达父 Job。在根 Coroutine (没有父 Coroutine,如 GlobalScope.launch 或在 runBlocking 内部直接 launch) 中未捕获的异常会导致应用崩溃,除非有 CoroutineExceptionHandler
  • 对于 async 启动的 Coroutine: 异常会在调用 await() 时抛出。这意味着 async 本身不会立即抛出异常,异常会被存储在 Deferred 对象中,直到你尝试获取结果。这使得你可以并行执行多个 async 任务,即使其中一个失败,也不会立即影响其他任务,直到你 await() 那个失败的结果。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println(“Caught unhandled exception: $exception”)
}

// Launch 异常处理
val job = GlobalScope.launch(handler) { // 注意:GlobalScope 是不推荐的,这里仅用于示例根 Coroutine
    println("Launching with exception")
    delay(100)
    throw RuntimeException("Exception from launch")
}
job.join() // 等待 job 完成(因异常而终止)

println("\nAsync exception handling")
val deferred = GlobalScope.async { // 注意:GlobalScope 不推荐
    println("Async with exception")
    delay(100)
    throw RuntimeException("Exception from async")
    "Result" // unreachable
}

try {
    val result = deferred.await() // 异常在这里抛出
    println("Async result: $result") // 这行不会执行
} catch (e: Exception) {
    println("Caught exception from async: ${e.message}")
}

}
“`

可能的输出:

“`
Launching with exception
Caught unhandled exception: java.lang.RuntimeException: Exception from launch

Async exception handling
Async with exception
Caught exception from async: Exception from async
“`

重要提示:

  • 结构化并发中的异常传播规则比较复杂。简单的规则是:子 Coroutine 的异常会传播给父 Job,并可能取消其他兄弟 Coroutine (取决于父 Job 是常规 Job 还是 SupervisorJob)。
  • supervisorScopeasync 提供了“隔离失败”的能力,即一个子任务的失败不会影响其他兄弟任务。
  • launch 中使用 CoroutineExceptionHandler 是处理未捕获根 Coroutine 异常的一种方式。
  • async 中使用 try/catch 包围 await() 是处理其异常的标准方式。

第六章:总结与展望

通过本文,我们快速入门了 Kotlin Coroutines,学习了以下核心概念:

  • suspend 函数: 标记可暂停和恢复的函数。
  • Coroutine 构建块: runBlocking (阻塞等待), launch (发后即忘), async/await (并发带结果)。
  • CoroutineContext Coroutine 的环境信息,包括 Job, Dispatcher 等。
  • CoroutineDispatcher 控制 Coroutine 执行的线程/线程池 (Default, IO, Main, Unconfined)。
  • withContext 在 Coroutine 执行过程中切换上下文。
  • 结构化并发: 使用 CoroutineScope 管理 Coroutine 生命周期,避免泄露,通过 Job 的父子关系实现自动传播取消。
  • coroutineScope vs supervisorScope 处理子协程异常传播的不同策略。
  • 取消: 协作式的取消机制,需要 Coroutine 配合(isActive, yield, delay 等)。
  • 异常处理: try/catch,以及 launchasync 不同的异常传播行为,CoroutineExceptionHandler

Coroutine 为 Kotlin 带来了全新的异步编程体验,它用同步风格的代码解决了异步和并发的复杂性,使得代码更加简洁、可读、易于维护。特别是在 Android 开发中,Coroutines 已成为官方推荐的异步解决方案,与 Lifecycle、ViewModel 等组件紧密集成。

这仅仅是 Coroutines 世界的冰山一角。Kotlin Coroutines 库还提供了更高级的并发原语,例如:

  • Flow: 用于处理异步数据流(类似于 RxJava 的 Observable)。
  • Channel: 用于 Coroutine 之间安全的通信。
  • Select: 用于等待多个挂起操作中的第一个完成。

掌握了本文介绍的基础知识,你就已经具备了使用 Kotlin Coroutines 处理日常异步任务的能力。继续深入学习更高级的特性,将能让你构建更强大、更复杂的并发应用。

祝你在 Coroutines 的世界里编程愉快!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部