Kotlin的协程
Kotlin 协程技术文档
简介
Kotlin 协程是 Kotlin 官方提供的一种用于简化异步编程和并发任务处理的工具。它能够让你用同步的写法实现异步代码,大大简化了回调地狱(callback hell)问题,提高了代码的可读性和维护性。协程在 Android 开发、服务器开发等场景中都有广泛应用。
协程基本概念
什么是协程
协程是一种轻量级的线程,能够在单个线程内并发执行多个任务。它基于挂起(suspending)和恢复(resuming)机制,在任务遇到耗时操作(如 IO、网络请求)时挂起执行,不阻塞线程,待条件满足后恢复执行。
协程与线程的对比
- 轻量性:协程比线程更加轻量,一个应用可以同时启动成千上万个协程,而线程数量通常受限于系统资源。
- 调度模型:协程由调度器(Dispatchers)管理,可以在多个线程间灵活调度,而线程调度依赖于操作系统。
- 切换成本:协程的上下文切换成本远低于线程切换,性能开销较小。
- 编程模型:协程可以用顺序化的代码编写异步逻辑,避免回调嵌套,使代码更直观。
协程构建块
CoroutineScope
- 定义:
CoroutineScope
表示一个协程作用域,它限定了协程的生命周期。所有在该作用域内启动的协程都遵循同一个生命周期,便于集中管理。 - 创建:可以通过
GlobalScope
(不推荐使用)、CoroutineScope(Job())
或 Android 的viewModelScope
、lifecycleScope
来创建作用域。
示例:
1 | // 自定义 CoroutineScope |
启动协程:launch 与 async
launch
用于启动一个不需要返回结果的协程,返回一个Job
对象。1
2
3
4
5myScope.launch {
// 在协程中执行耗时操作
delay(1000)
println("Hello from launch!")
}async
用于启动一个协程并返回一个Deferred
对象,表示未来可能返回的结果。常用于并行计算。1
2
3
4
5
6
7val deferredResult = myScope.async {
// 执行计算并返回结果
delay(500)
return@async 42
}
// 获取结果,注意:await() 是挂起函数
val result = deferredResult.await()
Job 与 Deferred
- Job:表示一个协程任务,主要用于管理协程的生命周期(如取消协程)。
- Deferred:继承自 Job,表示一个带有返回值的任务,使用
await()
方法可以挂起协程等待结果。
调度器 (Dispatchers)
调度器决定协程在哪个线程或线程池上运行。常用的调度器包括:
常见调度器
- Dispatchers.Main
用于更新 UI 的主线程,适用于 Android 中与 UI 相关的操作。 - Dispatchers.IO
用于执行 IO 密集型任务,如网络请求、磁盘操作等。 - Dispatchers.Default
用于执行 CPU 密集型任务,如复杂计算。 - Dispatchers.Unconfined
不受限于特定线程,立即在当前线程中执行,通常用于测试或特殊场景。
示例:
1 | // 在 IO 线程中执行网络请求 |
自定义调度器
可以创建自定义线程池,并用 asCoroutineDispatcher()
方法转换为协程调度器:
1 | val myExecutor = Executors.newFixedThreadPool(4) |
withContext()
的使用
withContext()
是 Kotlin 协程中的 挂起函数,用于 在协程中切换执行的调度器。它不会创建新的协程,而是挂起当前协程并在指定的调度器上执行代码块,执行完成后恢复到原来的调度器。
withContext()
会挂起外部协程,直到内部代码执行完毕,才继续执行外部协程的后续代码,但不会阻塞线程。
基本用法
1 | import kotlinx.coroutines.* |
执行过程
runBlocking
在 主线程 中运行。withContext(Dispatchers.IO)
切换到 IO 线程池 并执行代码块。delay(1000)
让协程挂起 1 秒,但不会阻塞线程。withContext
执行完毕,恢复到 原来的调度器(主线程)。
常见用途
1. 在 Dispatchers.Main
执行 UI 更新
适用于 Android 开发,用于 在后台线程执行任务,并回到主线程更新 UI。
1 | class MainActivity : AppCompatActivity() { |
🔹 fetchData()
在 IO 线程执行,获取数据后 withContext
自动 回到主线程 更新 textView
。
2. 计算密集型任务 (Dispatchers.Default
)
适用于 CPU 密集型计算(如排序、加密、数据处理)。
1 | suspend fun heavyComputation() { |
🔹 计算任务会在 默认线程池(Default
)运行,不会阻塞 UI。
3. 读取本地文件(IO 线程)
1 | suspend fun readFile(): String { |
🔹 避免主线程阻塞,文件读取在 IO 线程 完成。
⚠️ 注意事项
withContext()
不会创建新的协程,只是切换执行环境。withContext()
适合执行一个需要返回结果的任务,如果要 同时运行多个任务,用launch
或async
。- 不要在
Dispatchers.Main
中使用withContext(Dispatchers.Main)
,会导致 额外的调度开销。
🚀 总结
withContext()
用于 切换线程并执行代码,执行完自动回到原来的线程。- 适用于 IO 操作、计算任务、UI 更新等场景。
- 不会创建新协程,而是 挂起当前协程 并切换调度器。
💡 适用场景: ✅ 网络请求(Dispatchers.IO
)
✅ 数据库操作(Dispatchers.IO
)
✅ 计算密集型任务(Dispatchers.Default
)
✅ UI 更新(Dispatchers.Main
)
结构化并发
作用域与协程层级关系
结构化并发(Structured Concurrency)要求协程在层次化的作用域内启动,确保父协程管理子协程的生命周期,避免出现未管理的孤儿协程。这样可以保证资源的及时释放与错误的统一处理。
CoroutineScope 与 supervisorScope
CoroutineScope
默认情况下,如果子协程发生异常,会取消整个作用域内所有协程。supervisorScope
在supervisorScope
内,一个子协程的异常不会传播到其他子协程,可以实现部分任务失败而不影响整体流程。1
2
3
4
5
6
7
8supervisorScope {
val job1 = launch {
// 子任务 1
}
val job2 = launch {
// 子任务 2
}
}
协程取消与超时控制
取消协程
协程取消是一种协作机制,被取消的协程需要在合适的地方检测取消状态。常用方法包括:
- 调用
job.cancel()
:取消当前 Job 以及其所有子协程。 - 挂起点检测:例如
delay()
、withContext()
都会检查取消状态。
示例:
1 | val job = myScope.launch { |
withTimeout 与 withTimeoutOrNull
withTimeout
规定一段时间内未完成任务,则抛出TimeoutCancellationException
。1
2
3
4
5
6
7
8try {
withTimeout(3000) {
// 如果 3 秒内没有完成,则异常退出
performLongRunningTask()
}
} catch (e: TimeoutCancellationException) {
println("任务超时")
}withTimeoutOrNull
超时返回null
,不会抛异常。1
2
3
4
5
6val result = withTimeoutOrNull(3000) {
performLongRunningTask()
}
if (result == null) {
println("任务超时")
}
异常处理
CoroutineExceptionHandler
用于全局捕获协程未处理的异常。通过在协程上下文中添加 CoroutineExceptionHandler
,可以统一处理异常。
1 | val handler = CoroutineExceptionHandler { _, exception -> |
try/catch 在协程中的使用
在挂起函数内部,同步代码中的 try/catch
同样适用,用于捕获特定的异常。
1 | myScope.launch { |
协程通信与同步
Channel
Channel 提供了一种在协程之间进行通信的方式,类似于阻塞队列。通过 Channel,可以实现生产者-消费者模型。
示例:
1 | import kotlinx.coroutines.channels.Channel |
Flow
Flow 是 Kotlin 协程中的响应式流,用于处理异步数据流和背压控制。
- 定义 Flow:使用
flow {}
构建数据流 - 收集数据:使用
collect {}
处理数据
1 | import kotlinx.coroutines.flow.* |
与 Android 集成
在 Android 开发中,协程可以很好地解决异步任务和 UI 更新问题。常见的集成方式包括:
在 UI 线程中启动协程
利用 Dispatchers.Main
在主线程中更新 UI:
1 | CoroutineScope(Dispatchers.Main).launch { |
使用 ViewModelScope 和 LifecycleScope
viewModelScope
适用于 ViewModel 中启动协程,与 ViewModel 生命周期绑定,避免内存泄漏。1
2
3
4
5
6
7class MyViewModel : ViewModel() {
init {
viewModelScope.launch {
// 执行网络请求或数据加载
}
}
}lifecycleScope
适用于 Activity 或 Fragment,自动管理协程生命周期。1
2
3
4
5
6
7
8class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// 执行与 UI 相关的协程操作
}
}
}
最佳实践与常见问题
最佳实践
- 避免使用 GlobalScope
全局作用域容易导致协程泄漏,建议使用结构化并发的 CoroutineScope。 - 选择合适的调度器
根据任务类型(IO、CPU、UI)选择对应的调度器。 - 合理使用超时与取消
对长时间等待的任务使用 withTimeout/withTimeoutOrNull,确保协程及时取消。 - 异常捕获
在关键业务逻辑中使用 try/catch,同时配置 CoroutineExceptionHandler 捕获全局异常。 - 整洁代码
将协程相关代码放入专门的 Repository 或 UseCase 层,分离 UI 层逻辑。
常见问题
- 协程泄漏
未取消的协程会导致内存泄漏。确保在合适时机调用 cancel() 或使用绑定生命周期的 Scope。 - 异常未捕获
子协程异常可能不会传递给父协程,使用 supervisorScope 或者合适的异常处理器来保证异常处理一致性。 - 线程切换问题
协程中的上下文切换需要显式指定 withContext,否则可能导致 UI 阻塞或后台线程操作 UI。