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 框架会负责在 getUserIdAsync
、getUserInfoAsync
、getOrdersAsync
等函数执行 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:决定执行在哪
CoroutineDispatcher
是 CoroutineContext
中的一个关键元素,它决定了 Coroutine 在哪个线程或线程池上执行。Kotlin Coroutines 提供了几种内置的调度器:
Dispatchers.Default
: 默认调度器,用于 CPU 密集型任务。它使用一个共享的后台线程池,线程数量默认等于 CPU 核心数。Dispatchers.IO
: 用于 I/O 密集型任务,如网络请求、文件读写、数据库操作。它使用一个更大的共享线程池,线程数量会根据需要增长,但有上限。推荐将大部分耗时的阻塞 I/O 操作切换到此调度器。Dispatchers.Main
: (仅在支持的平台如 Android 或 JavaFX 中可用) 主线程调度器,用于与 UI 交互。所有 UI 更新都必须在此调度器上执行。Dispatchers.Unconfined
: 不受限调度器。它在调用它的当前线程上启动 Coroutine,但 Coroutine 第一次遇到suspend
点之后,可能会在任何其他合适的线程上恢复执行。不推荐在代码中使用,主要用于一些特殊场景或测试。
你可以通过在 launch
或 async
的第一个参数中指定调度器来切换 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 中使用 launch
或 async
创建新的 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 失败的影响
“`
理解 coroutineScope
和 supervisorScope
的区别对于构建健壮的并发程序至关重要。
第五章:取消与异常处理
Coroutine 的取消和异常处理是结构化并发的重要组成部分。
5.1 Coroutine 的取消
Coroutine 的取消是 协作式 的。这意味着 Coroutine 本身必须配合取消操作才能真正停止执行。大多数 Kotlin Coroutine 库提供的 suspend
函数(如 delay
, yield
, withContext
, withTimeout
等)都是可取消的,它们在被取消时会抛出 CancellationException
。
如果你在一个 Coroutine 中执行长时间运行的 非协作 计算(如一个无限循环或一个复杂的计算),并且没有在循环内部检查取消状态,那么这个 Coroutine 将无法被外部取消。
检查取消状态的方法:
- 使用
isActive
属性(它是CoroutineScope
的扩展属性,通过coroutineContext[Job]
获取)。 - 使用
ensureActive()
函数,如果 Coroutine 不活跃(已取消),它会抛出CancellationException
。 - 使用
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)
循环中使用 delay
或 yield
是使计算代码可取消的常用模式。如果你的计算是纯 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)。
supervisorScope
和async
提供了“隔离失败”的能力,即一个子任务的失败不会影响其他兄弟任务。- 在
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
vssupervisorScope
: 处理子协程异常传播的不同策略。- 取消: 协作式的取消机制,需要 Coroutine 配合(
isActive
,yield
,delay
等)。 - 异常处理:
try/catch
,以及launch
和async
不同的异常传播行为,CoroutineExceptionHandler
。
Coroutine 为 Kotlin 带来了全新的异步编程体验,它用同步风格的代码解决了异步和并发的复杂性,使得代码更加简洁、可读、易于维护。特别是在 Android 开发中,Coroutines 已成为官方推荐的异步解决方案,与 Lifecycle、ViewModel 等组件紧密集成。
这仅仅是 Coroutines 世界的冰山一角。Kotlin Coroutines 库还提供了更高级的并发原语,例如:
- Flow: 用于处理异步数据流(类似于 RxJava 的 Observable)。
- Channel: 用于 Coroutine 之间安全的通信。
- Select: 用于等待多个挂起操作中的第一个完成。
掌握了本文介绍的基础知识,你就已经具备了使用 Kotlin Coroutines 处理日常异步任务的能力。继续深入学习更高级的特性,将能让你构建更强大、更复杂的并发应用。
祝你在 Coroutines 的世界里编程愉快!