一文带你了解 Kotlin Coroutines:协程的奥秘与实战
在现代软件开发中,尤其是涉及到网络请求、数据库操作、文件读写等耗时任务时,如何有效地进行并发和异步编程是一个绕不开的话题。传统的线程模型存在诸多挑战,如资源消耗大、上下文切换开销高、以及臭名昭著的“回调地狱”(Callback Hell)和复杂的线程管理。Kotlin 协程(Coroutines)应运而生,它为 Kotlin 带来了轻量级、易于使用的并发解决方案,极大地简化了异步编程的复杂性。
本文将带你深入了解 Kotlin Coroutines 的核心概念、工作原理、常见用法和最佳实践,让你能够充分利用这一强大工具。
1. 异步编程的痛点:我们为什么需要协程?
在探讨协程之前,我们先回顾一下传统的异步编程方式及其面临的问题:
-
多线程 (Threads):
- 优点: 能够利用多核 CPU 提高程序吞吐量。
- 缺点:
- 资源消耗大: 每个线程都需要独立的栈空间、程序计数器等,创建和维护开销较大。一个应用能创建的线程数量有限(通常几千个)。
- 上下文切换开销: CPU 在不同线程间切换时,需要保存和恢复线程的状态,频繁切换会损耗性能。
- 同步复杂: 共享数据需要锁机制(
synchronized
,Lock
等),容易引发死锁、活锁、竞态条件等问题,调试困难。 - 生命周期管理困难: 手动管理线程的启动、停止、中断等状态,容易出现资源泄露。
-
回调 (Callbacks):
- 优点: 简单直接,适用于简单的异步操作。
- 缺点:
- 回调地狱 (Callback Hell): 多个异步操作依赖前一个的结果时,代码会层层嵌套,逻辑结构扁平化,难以阅读、理解和维护。
- 错误处理分散: 每个回调都需要单独处理错误。
- 状态难以维护: 在不同回调之间传递和维护状态很麻烦。
-
Futures/Promises:
- 优点: 解决了回调地狱的部分问题,通过链式调用使代码更线性。
- 缺点:
- 组合复杂: 对于更复杂的控制流(如条件分支、循环、取消),组合 Futures/Promises 仍然不够直观。
- 仍然是基于事件驱动: 本质上还是通过注册回调来处理结果或错误。
这些传统方式在应对现代应用中日益增长的并发需求时显得力不从心。我们需要一种新的范式,既能实现高效的并发,又能让代码逻辑像同步代码一样简洁易读。
2. 初识 Kotlin Coroutines:什么是协程?
Kotlin 协程是一种轻量级的并发框架,它允许你编写非阻塞、异步的代码,但代码结构看起来像同步执行一样。
最直观的理解是:协程是比线程更轻量级的执行单元。
- 轻量级: 启动数千甚至数百万个协程是可行的,而线程通常只能创建几千个。协程栈空间小,创建和销毁开销极小。
- 非阻塞: 协程通过“挂起”(Suspending)而不是“阻塞”(Blocking)来等待结果。当一个协程遇到耗时操作时,它不会阻塞所在的线程,而是会将自己的执行状态(包括局部变量、程序计数器等)保存起来,挂起。当耗时操作完成时,协程可以在同一个线程或另一个线程上恢复执行,从之前挂起的地方继续。
- 结构化: Kotlin 协程提倡“结构化并发”(Structured Concurrency),通过父子关系管理协程的生命周期,使得错误传播和取消更加可控和安全。
- 像同步代码: 这是协程最吸引人的特性之一。通过特殊的
suspend
函数和协程构建器,你可以用看起来像传统顺序执行的代码来表达复杂的异步逻辑。
类比:
- 线程: 就像操作系统调度的工人,他们有独立的办公室(内存空间),切换办公室需要时间(上下文切换)。工人数量有限,太多会挤占空间。
- 协程: 就像在同一个办公室工作的多个任务(或多个“工作流”)。当一个任务需要等待(比如等人送材料过来),它就把当前做到哪一步记下来,让出办公桌给其他任务继续工作,而不是坐在那里发呆(阻塞线程)。等材料来了,它再回到办公桌,从上次暂停的地方继续。同一个办公室(同一个线程)可以容纳非常多的任务。
3. 协程 vs. 线程:深入对比
特性 | 线程 (Thread) | 协程 (Coroutine) |
---|---|---|
调度者 | 操作系统 (OS scheduler) | 协程框架/调度器 (Coroutine dispatcher) 在线程上调度 |
开销 | 高 (创建、销毁、内存占用) | 低 (创建、销毁、内存占用) |
数量 | 有限 (几千个) | 大量 (几百万个或更多) |
切换 | 上下文切换 (Context Switching),由 OS 完成,开销高 | 挂起/恢复 (Suspending/Resuming),由协程框架完成,开销低 |
阻塞 | 阻塞 (Blocking) 线程直到任务完成 | 挂起 (Suspending) 自身,不阻塞线程 |
栈 | 大 (通常几 MB) | 小 (通常几十 KB 或更小,可动态增长) |
取消 | 困难,非合作式 (使用中断标志等),需要手动检查 | 合作式 (Cooperative Cancellation),更易于管理 |
生命周期 | 手动管理复杂 | 结构化并发 (Structured Concurrency),父子关系管理更安全 |
异常处理 | 需要手动处理,跨线程传递复杂 | 结构化并发下异常传播更可控 |
编程风格 | 回调、Futures/Promises、锁等,代码结构易复杂化 | 看起来像同步代码,通过 suspend 关键字实现异步 |
核心区别在于: 线程是操作系统的调度单位,协程是用户空间的调度单位。协程在线程上运行,一个线程可以运行多个协程。协程通过挂起和恢复来切换,而线程通过上下文切换来切换。
4. Kotlin Coroutines 的核心要素
理解 Kotlin Coroutines 需要掌握几个关键概念:
4.1. suspend
函数:协程的暂停与恢复点
suspend
关键字是协程的核心。它只能用于标记函数,表示这个函数可能是一个挂起点(suspension point)。
- 当协程执行到一个
suspend
函数内部的耗时操作(例如网络请求、延迟等)时,它不会阻塞当前的线程,而是会将自己的执行状态(包括局部变量、下一条要执行的指令等)保存起来,然后将当前线程让给其他协程或任务使用。 - 当耗时操作完成后,
suspend
函数会在合适的时机(由协程调度器决定)恢复协程的执行,从之前挂起的地方继续往下运行。
注意: suspend
函数本身并不会启动新的协程或线程。它只是一个标记,告诉编译器这个函数是可以被挂起和恢复的。suspend
函数只能在其他 suspend
函数内部或者在协程作用域(Coroutine Scope)内调用。
示例:
“`kotlin
import kotlinx.coroutines.*
// 一个模拟耗时操作的 suspend 函数
suspend fun doNetworkRequest(url: String): String {
println(“模拟网络请求开始: $url”)
delay(2000) // Coroutines 提供的非阻塞延迟函数
println(“模拟网络请求结束: $url”)
return “Response from $url”
}
fun main() = runBlocking { // runBlocking 是一个协程构建器,创建一个阻塞当前线程的协程作用域
println(“主程序开始”)
val result = doNetworkRequest(“https://example.com”) // 在协程作用域内调用 suspend 函数
println(“接收到结果: $result”)
println(“主程序结束”)
}
“`
上面的 main
函数使用了 runBlocking
协程构建器,创建了一个协程作用域。在 runBlocking
内部,我们可以调用 doNetworkRequest
这个 suspend
函数。当执行到 delay(2000)
时,协程会挂起,runBlocking
所在的线程不会被阻塞。2秒后,协程恢复执行。整个过程看起来就像同步代码一样顺序执行,但底层是非阻塞的。
编译器会特殊处理 suspend
函数,将其转换为带有状态机的代码,以便在挂起和恢复时保存和恢复状态。
4.2. CoroutineScope:管理协程的生命周期
CoroutineScope
定义了协程的生命周期和结构化并发。它是一个接口,用于将协程与其生命周期绑定。
-
结构化并发: 在一个
CoroutineScope
中启动的协程(子协程)会自动链接到该 Scope(父协程)。- 父协程取消时,所有子协程也会被取消。
- 子协程失败时,会传播异常给父协程,可能导致父协程和其兄弟协程被取消(默认行为,可通过
SupervisorJob
修改)。 - 父协程会等待所有子协程完成后才算完成。
-
生命周期管理: 通过管理 Scope 的生命周期,可以方便地管理其内部所有协程的生命周期。例如,在 Android 开发中,可以将 CoroutineScope 绑定到 ViewModel 或 Activity 的生命周期,当 ViewModel/Activity 销毁时,取消相应的 Scope,从而取消所有与其相关的协程,避免内存泄露和不必要的工作。
创建 CoroutineScope:
- 预定义的 Scope:
GlobalScope
: 全局 Scope,生命周期与应用绑定。不推荐直接使用,因为它不会结构化地管理协程的生命周期,容易导致协程泄露或难以取消。runBlocking
: 用于阻塞当前线程直到其内部协程完成,主要用于 main 函数或测试。
- 自定义 Scope: 通常结合
CoroutineContext
创建,例如:val scope = CoroutineScope(Dispatchers.Main + Job())
。
示例:
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking { // 这是外部 Scope (runBlocking)
val job = launch { // 这是 runBlocking Scope 的一个子协程
repeat(5) { i ->
println(“子协程打印 $i”)
delay(500)
}
}
println(“主协程等待子协程完成…”)
job.join() // 等待子协程完成
println(“子协程完成,主协程结束”)
}
“`
在上面的例子中,launch
创建的协程是 runBlocking
协程的子协程。runBlocking
会等待 launch
创建的子协程完成后再结束。如果我们在 main
函数中调用 job.cancel()
,子协程也会被取消。
4.3. CoroutineContext:协程的上下文信息
CoroutineContext
是一个元素的集合,它定义了协程的运行环境。主要元素包括:
Job
: 控制协程的生命周期(启动、取消、完成)以及父子关系。CoroutineDispatcher
: 决定协程在哪个线程或线程池上执行。CoroutineName
(可选): 协程的名称,方便调试。CoroutineExceptionHandler
(可选): 处理未捕获的异常。
你可以使用 +
操作符组合不同的上下文元素来创建一个新的 CoroutineContext
。
示例:
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 组合不同的上下文元素
val context = Dispatchers.IO + CoroutineName(“MyWorker”)
val job = launch(context) { // 使用指定的上下文启动协程
println("我在线程: ${Thread.currentThread().name}, 名称: ${coroutineContext[CoroutineName]?.name}")
// 执行 IO 密集型任务...
}
job.join()
}
“`
4.4. CoroutineDispatcher:决定协程运行的线程
CoroutineDispatcher
决定了协程将在哪个线程上执行。Kotlin Coroutines 提供了几种标准的调度器:
Dispatchers.Main
: 主线程调度器,用于在 UI 线程上执行协程(例如在 Android 中更新 UI)。只能在有主线程(如 Android、Swing、JavaFX)的环境中使用。Dispatchers.Default
: 默认调度器,适用于 CPU 密集型任务。它使用一个由 JVM 维护的共享线程池,线程数默认为 CPU 核数。Dispatchers.IO
: 适用于 IO 密集型任务,如网络请求、文件读写、数据库操作等。它使用一个共享的线程池,按需创建新线程,但会限制线程数量(通常是 Default 线程数的几倍,上限较高)。Dispatchers.Unconfined
: 非受限调度器。协程在调用者线程上启动,但遇到第一个挂起点后,会在恢复它的线程上继续执行。不推荐在普通应用代码中使用,因为它会导致协程的执行线程难以预测。
选择合适的 Dispatcher 至关重要:
- CPU 密集型任务: 使用
Dispatchers.Default
。 - IO 密集型任务: 使用
Dispatchers.IO
。 - UI 更新: 使用
Dispatchers.Main
。 - 复杂的后台逻辑(可能包含多种类型任务): 通常从
Dispatchers.Default
或Dispatchers.IO
开始,并在需要切换线程时使用withContext
。
切换 Dispatcher (withContext
):
你可以在一个协程内部使用 withContext
函数方便地切换协程的执行上下文(主要是 Dispatcher)。
“`kotlin
import kotlinx.coroutines.*
suspend fun fetchData(): String {
println(“在 ${Thread.currentThread().name} 开始获取数据”)
return withContext(Dispatchers.IO) { // 切换到 IO 线程执行
println(“在 ${Thread.currentThread().name} 执行 IO 操作”)
delay(2000) // 模拟网络延迟
“远程数据”
}
}
fun updateUI(data: String) {
println(“在 ${Thread.currentThread().name} 更新 UI: $data”)
// 模拟 UI 更新操作
}
fun main() = runBlocking {
// 假设 runBlocking 运行在主线程 (或者你可以用 Dispatchers.Main)
println(“在 ${Thread.currentThread().name} 启动”)
val result = fetchData() // 调用 suspend 函数,内部会切换线程
updateUI(result) // 回到启动时的线程更新 UI
println(“在 ${Thread.currentThread().name} 结束”)
}
“`
withContext
是一个 suspend 函数。它会暂停当前协程的执行,将执行上下文切换到指定的 Dispatcher,执行其 lambda 块中的代码,然后将结果返回,并将协程的执行上下文切回原来的 Dispatcher。这使得在不同类型的任务之间切换线程变得非常简洁和安全。
4.5. Job:协程的生命周期和层次结构
Job
是协程的句柄,代表了一个正在运行的协程或一个未来会完成的任务。它有自己的生命周期(New, Active, Cancelling, Cancelled, Completing, Completed)并提供控制协程的方法:
start()
: 启动协程(如果它是懒启动的)。cancel()
: 请求取消协程。join()
: 挂起当前协程,直到 Job 完成。isActive
: 检查协程是否处于活动状态。isCancelled
: 检查协程是否已请求取消。isCompleted
: 检查协程是否已完成。
Job
也是 CoroutineContext
的一部分,通过它可以实现结构化并发的父子关系。当你使用 launch
或 async
等构建器在一个 CoroutineScope
中启动协程时,会返回一个 Job
(对于 launch
)或 Deferred
(对于 async
,它是 Job
的子类)。这个新的 Job 会成为 Scope 对应 Job 的子 Job。
4.6. 协程构建器:launch
vs. async
启动协程通常使用协程构建器函数:
-
launch
: 用于启动一个协程,执行一个不需要返回结果的任务。它返回一个Job
对象,你可以用它来管理协程的生命周期(如取消、等待完成)。如果launch
块内部抛出异常,并且没有捕获,该异常会传播到父协程并可能导致整个协程家族被取消。“`kotlin
import kotlinx.coroutines.*fun main() = runBlocking {
val job = launch { // 启动一个协程
delay(1000)
println(“Hello”) // 1秒后打印
}
println(“World”) // 立即打印
job.join() // 等待协程完成
println(“Done”)
}
“` -
async
: 用于启动一个协程,执行一个需要返回结果的任务。它返回一个Deferred<T>
对象,它是Job
的子类,但额外提供了await()
挂起函数来获取结果。如果async
块内部抛出异常,该异常会在调用await()
时重新抛出。“`kotlin
import kotlinx.coroutines.*suspend fun performTask(): String {
delay(1000)
return “Result”
}fun main() = runBlocking {
val deferred: Deferred= async { // 启动一个协程,期望返回 String
performTask()
}
println(“Waiting for result…”)
val result = deferred.await() // 挂起当前协程,直到 async 协程完成并返回结果
println(“Got result: $result”)
}
“`
选择 launch
还是 async
?
- 如果你只需要执行一个任务,不需要它的结果,使用
launch
。 - 如果你需要执行一个任务,并且需要等待它的结果进行后续处理,使用
async
和await()
。
并行执行多个任务 (async
和 awaitAll
):
async
特别适合并行执行多个独立的异步任务:
“`kotlin
import kotlinx.coroutines.*
suspend fun fetchUserData(): String { delay(1000); return “User Data” }
suspend fun fetchOrderData(): String { delay(1500); return “Order Data” }
fun main() = runBlocking {
println(“开始并行获取数据…”)
val userDeferred = async { fetchUserData() } // 启动第一个任务
val orderDeferred = async { fetchOrderData() } // 启动第二个任务
// await() 挂起当前协程直到对应的 deferred 完成
// awaitAll() 是 await() 的变种,等待所有 deferred 完成
val userData = userDeferred.await()
val orderData = orderDeferred.await()
println("所有数据获取完成:")
println(userData)
println(orderData)
}
“`
在这个例子中,fetchUserData
和 fetchOrderData
会几乎同时开始执行(由 Dispatcher 决定)。await()
调用会等待各自的任务完成,但因为它们是并行执行的,总的等待时间取决于最慢的任务。
5. 协程的取消 (Cancellation)
协程的取消是合作式(Cooperative)的。这意味着一个正在运行的协程必须主动配合取消请求才能被取消。协程库中的所有标准挂起函数(如 delay
, yield
, withContext
, withTimeout
等)都是可取消的:当协程被取消时,如果在这些函数的调用点,它们会检查取消状态并抛出 CancellationException
,从而使协程停止执行。
如果你的协程在执行计算密集型任务或者其他不包含挂起点(或包含第三方不可取消的阻塞调用)的代码,它不会自动响应取消请求。你需要定期检查协程的取消状态:
- 通过
isActive
属性(coroutineContext.isActive
或直接在CoroutineScope
中访问)。 - 调用
ensureActive()
函数:如果协程已不再活跃,它会抛出CancellationException
。 - 调用
yield()
函数:这是一个挂起函数,它会检查取消状态,并有机会让出执行权给其他协程。
示例:手动检查取消状态
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) { // 在 Default 线程进行计算
var nextPrintTime = startTime
var i = 0
while (isActive) { // 检查活跃状态
// 计算密集型任务
if (System.currentTimeMillis() >= nextPrintTime) {
println(“子协程: 我还在工作 ${i++} …”)
nextPrintTime += 500
}
}
println(“子协程: 已被取消.”) // 检查到 isActive 为 false 后跳出循环
}
delay(1300) // 等待一段时间
println("主协程: 我要取消子协程了...")
job.cancelAndJoin() // 取消并等待子协程结束
println("主协程: 子协程已取消,主协程结束.")
}
“`
在处理文件、网络等资源时,在取消协程时进行资源清理非常重要。可以使用 finally
块来确保资源在协程结束(无论是正常完成还是被取消)时得到释放。协程框架确保 finally
块在 CancellationException
抛出时也会执行。
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println(“我正在运行 $i …”)
delay(100) // 这是一个可取消的挂起点
}
} catch (e: CancellationException) {
println(“异常捕获: 协程被取消了!”)
} finally {
println(“清理工作: 关闭资源…”)
// 在这里释放文件句柄、网络连接等
}
}
delay(250) // 等待一小段时间
println("我要取消它!")
job.cancelAndJoin() // 取消并等待
println("Done.")
}
“`
6. 异常处理 (Exception Handling)
在结构化并发中,异常处理遵循一定的规则:
launch
构建器: 未捕获的异常会向上冒泡传播给父协程。如果父协程没有特殊处理(如使用SupervisorJob
),它会取消自己和所有其他子协程。顶层的未捕获异常会通过CoroutineExceptionHandler
(如果提供了)或默认的线程异常处理器来处理。async
构建器: 异常会在调用await()
时才会被重新抛出。如果await()
没有被调用,异常默认是隐藏的(但协程状态会变为失败)。这使得可以在调用点集中处理由async
启动的任务中的错误。
使用 CoroutineExceptionHandler
:
CoroutineExceptionHandler
是 CoroutineContext
的一个元素,可以用来处理由 launch
启动的顶层协程中未捕获的异常。它只对由 launch
创建的根协程(或者在 SupervisorJob
下的子协程)有效。
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println(“捕获到异常: $exception”)
}
// 在 launch 构建器中添加 handler
val job = launch(handler) {
println("抛出异常前...")
throw RuntimeException("Something went wrong")
println("抛出异常后 (不会执行)")
}
job.join() // 等待协程完成/失败
println("协程结束")
}
“`
使用 SupervisorJob
:
默认的 Job
在子协程失败时会取消父协程和所有兄弟协程。但有时你希望子协程之间是独立的,一个失败不影响其他协程。这时可以使用 SupervisorJob
。在 SupervisorJob
下,子协程的失败不会向下传播给兄弟协程或向上传播给父协程(但异常仍会冒泡,需要由子协程自身或其 CoroutineExceptionHandler
处理)。
通常通过 SupervisorJob()
或 SupervisorJob()
结合 CoroutineScope
创建一个有监督的 Scope:
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisorScope = CoroutineScope(SupervisorJob()) // 创建一个监督 Scope
val job1 = supervisorScope.launch {
delay(500)
println("Job 1 完成")
}
val job2 = supervisorScope.launch {
delay(200)
println("Job 2 抛出异常")
throw RuntimeException("失败了!")
}
val job3 = supervisorScope.launch {
delay(1000)
println("Job 3 完成")
}
delay(300) // 等待 job2 失败
println("Job 2 失败后:")
job1.join() // Job 1 不受影响,可以正常完成
// job2 已经失败
job3.join() // Job 3 也不受影响,可以正常完成
println("所有任务结束")
supervisorScope.cancel() // 最后取消监督 Scope
}
“`
注意: SupervisorJob
只改变了异常的传播方向(不向上和向下)。异常本身仍然存在,需要被捕获或通过 CoroutineExceptionHandler
处理。在 supervisorScope.launch
中,如果异常没有被捕获,它会向上冒泡到监督 Scope,但不会导致监督 Scope 取消。然而,你仍然需要一个 CoroutineExceptionHandler
来防止未捕获异常导致应用崩溃(在某些平台如 Android)。或者,更常见的模式是在 supervisorScope.launch
块内部使用 try/catch
来处理可能的异常。
7. 常见使用模式和示例
-
UI 编程 (Android):
kotlin
// 在 ViewModel 中
class MyViewModel : ViewModel() {
// 使用 ViewModel 的 viewModelScope,它绑定到 ViewModel 生命周期,使用 Dispatchers.Main
fun loadData() {
viewModelScope.launch {
// 在主线程开始
try {
val data = withContext(Dispatchers.IO) {
// 切换到 IO 线程执行耗时操作
fetchDataFromServer()
}
// 回到主线程更新 LiveData 或 UI
_myData.value = data
} catch (e: Exception) {
// 处理错误,回到主线程通知 UI
_error.value = e.message
}
}
}
} -
后台任务 (非 UI):
kotlin
suspend fun processLargeFile(filePath: String) = withContext(Dispatchers.IO) {
// 在 IO 线程打开和读取文件
File(filePath).bufferedReader().use { reader ->
reader.lineSequence().forEach { line ->
// 在 Default 线程进行处理,避免 IO 线程被计算阻塞
withContext(Dispatchers.Default) {
processLine(line)
}
}
}
} -
带超时控制的任务:
“`kotlin
import kotlinx.coroutines.*suspend fun fetchDataWithTimeout(): String = withTimeout(3000) { // 3秒超时
println(“开始获取数据…”)
delay(4000) // 模拟耗时操作超过超时时间
“数据获取成功” // 如果没超时会返回这个
}fun main() = runBlocking {
try {
val result = fetchDataWithTimeout()
println(result)
} catch (e: TimeoutCancellationException) {
println(“数据获取超时!”)
}
}
``
withTimeout和
withTimeoutOrNull` (返回 null 而不是抛异常) 非常实用。
8. 更高级的主题 (简述)
- Flow: Kotlin Flow 是用于处理异步数据流的强大工具。它可以发射多个值,支持各种转换操作,并且是冷流(Cold Stream),只有在被收集时才执行。Flow 是协程与反应式编程结合的产物。
- Channel: Channel 是一种用于协程之间通信的机制,类似于阻塞队列(BlockingQueue),但它是非阻塞的。它可以用于在不同协程之间安全地传递数据。
这些高级主题本身就值得深入探讨,通常在你掌握了基础的协程概念后才会接触。
9. 最佳实践
- 始终使用结构化并发: 在
CoroutineScope
中启动协程,避免使用GlobalScope
。将 Scope 的生命周期与你的组件(Activity, ViewModel, Presenter 等)绑定。 - 选择合适的 Dispatcher: 根据任务类型(UI, IO, CPU)选择最合适的调度器。
- 使用
withContext
切换 Dispatcher: 在同一个协程中需要在不同线程类型之间切换时,优先使用withContext
。 - 确保协程是可取消的: 在长时运行的计算密集型任务中定期检查
isActive
或调用yield()
。在资源清理时使用finally
块。 - 正确处理异常: 理解
launch
和async
的异常传播规则。根据需要使用CoroutineExceptionHandler
或SupervisorJob
,并使用try/catch
捕获异常。 - 避免在协程中进行阻塞调用: 如果必须进行阻塞调用(例如调用一个旧的同步 API),请确保在
Dispatchers.IO
中执行,并且考虑将其包装成suspend
函数。 - 最小化协程的作用范围: 只在你需要执行异步/并发操作的地方引入协程,避免滥用。
10. 总结
Kotlin Coroutines 通过引入 suspend
函数、协程构建器和结构化并发等概念,极大地简化了异步和并发编程。它们提供了比传统线程更轻量级、更灵活、更易于管理和调试的方案。
通过本文的介绍,你应该对 Kotlin Coroutines 有了一个比较全面的认识:
- 理解了异步编程的痛点以及协程如何解决这些问题。
- 掌握了
suspend
函数的含义和作用。 - 理解了
CoroutineScope
在生命周期管理和结构化并发中的重要性。 - 了解了
CoroutineContext
的组成部分,特别是CoroutineDispatcher
的作用和选择。 - 区分了
launch
和async
两个主要的协程构建器及其用途。 - 学习了协程的合作式取消机制和异常处理策略。
掌握 Kotlin Coroutines 是现代 Kotlin 开发者的必备技能,尤其是在 Android、后端服务(如 Ktor)、桌面应用等领域。开始在你的项目中尝试使用协程吧,你会发现异步代码可以变得前所未有的简洁和高效!