2. Kotlin协程
以下是关于 Kotlin 协程的深度解析,包含核心概念、线程对比、使用方式、工作原理及最佳实践:
一、协程的本质与核心概念
定义:协程是一种轻量级的线程替代方案,由程序控制调度,而非操作系统。它允许代码暂停(suspend
)和恢复执行,无需阻塞线程。
核心特性:
- 轻量级:单个线程可运行数千个协程,内存占用仅 ~1KB(对比线程的 MB 级)。
- 非阻塞:协程挂起时不阻塞线程,线程可执行其他任务。
- 结构化并发:通过作用域管理协程生命周期,避免内存泄漏。
- 挂起函数:使用
suspend
标记的函数,可暂停和恢复执行。
二、协程 vs 线程
特性 | 协程(Coroutine) | 线程(Thread) |
---|---|---|
调度 | 由程序(协程调度器)控制 | 由操作系统内核调度 |
创建成本 | 极低(约 1KB 内存) | 高(约 1MB 内存,视平台而定) |
切换开销 | 极小(纳秒级,仅涉及上下文切换) | 高(微秒级,涉及内核态切换) |
并发性 | 单线程可运行数千个协程 | 受限于系统资源(通常数百个) |
阻塞影响 | 仅挂起当前协程,不影响线程 | 阻塞整个线程,其他任务需等待 |
适用场景 | I/O 密集型任务(如网络请求) | CPU 密集型任务(如计算) |
三、协程的基本使用姿势
1. 启动协程的方式
所有的协程必须在一个作用域内执行,使用方式为
作用域.launch {}
其中 launch 是一个函数,用于创建协程并将其函数主体的执行分派给相应的调度程序
Dispatchers.IO 指示此协程应在为 I/O 操作预留的线程上执行。
一个基础的使用示例:
import kotlinx.coroutines.*
// 1. GlobalScope.launch:非结构化,谨慎使用
GlobalScope.launch {
delay(1000) // 非阻塞延迟
println("GlobalScope 协程")
}
// 2. runBlocking:阻塞当前线程,用于测试
runBlocking {
launch { // 默认继承 runBlocking 的协程作用域
println("runBlocking 内部协程")
}
}
// 3. CoroutineScope.launch:推荐方式(结构化并发)
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
// 协程逻辑
}
scope.cancel() // 取消所有子协程
2. 挂起函数(Suspend Functions)
在函数前添加关键字suspend
,声明这个函数为挂起函数; 若我们希望两个挂起函数顺序调用,在协程中,按照正常的顺序书写即可,后面的挂起函数可以正常获取前面的挂起函数,即两者是顺序执行的
suspend fun fetchData(): String {
delay(1000) // 模拟网络请求
return "Data"
}
// 在协程中调用挂起函数
launch {
val data = fetchData()
println(data)
}
3. 异步任务与结果获取
若两个挂起函数之间没有依赖,我希望它们能并行调度,方便我快速获取结果,此时可以借助 async
来实现
在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。
suspend fun main() = coroutineScope {
val deferred1 = async { loadData1() } // 启动异步任务
val deferred2 = async { loadData2() }
val result = deferred1.await() + deferred2.await() // 获取结果
}
四、协程的核心组件
1. 协程作用域(CoroutineScope)
管理协程的生命周期,确保资源正确释放:
class MyViewModel : ViewModel() {
// 使用 viewModelScope(Android 架构组件提供)
fun fetchData() = viewModelScope.launch {
// 协程逻辑
}
}
在 Android 开发过程中,我们需要理解一些协程代码运行的范围。而所有的Scope 如 GlobalScope
都是 CoroutineScope
的子类,我们的协程创建都需要这样一个 CoroutineScope
来启动。
一些常见的作用域 CoroutineScope
对象。
- GlobeScope:全局范围,不会自动结束执行。
- MainScope:主线程的作用域,全局范围
- lifecycleScope:生命周期范围,用于activity等有生命周期的组件,在DESTROYED的时候会自动结束。
- viewModelScope:viewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束
手动创建一个作用域
val scope = CoroutineScope(Dispatchers.IO)
2. 协程调度器(Dispatchers)
指定协程执行的线程池:
Dispatchers.Main
:主线程(UI 线程),用于更新 UI。Dispatchers.IO
:适合 I/O 密集型任务(默认 64 线程)。Dispatchers.Default
:适合 CPU 密集型任务(默认线程数为 CPU 核心数)。newSingleThreadContext
:创建专用单线程。
3. Job 与协程生命周期
val job = launch {
// 协程体
}
job.start() // 启动协程
job.cancel() // 取消协程
job.join() // 等待协程完成
五、协程的工作原理
挂起与恢复:
- 协程通过状态机实现挂起,将局部变量保存在对象中。
- 挂起时释放线程,恢复时从上次暂停处继续执行。
Continuation Passing Style (CPS):
编译器将
suspend
函数转换为带Continuation
参数的状态机。示例:
// 原始代码 suspend fun main() { println("Before") delay(1000) println("After") } // 编译后(简化) fun main(continuation: Continuation<Unit>): Any { when (continuation.label) { 0 -> { println("Before") return delay(1000, continuation.copy(label = 1)) } 1 -> { println("After") return Unit } } }
调度器工作流程:
- 协程调度器管理线程池,将协程任务分发给空闲线程。
- 非阻塞操作(如
delay
)通过回调机制恢复执行。
六、协程的异常处理
1. 结构化异常处理
// 使用 try-catch
launch {
try {
fetchData()
} catch (e: Exception) {
// 处理异常
}
}
// 使用 supervisorScope 隔离子协程异常
supervisorScope {
launch { /* 可能抛出异常的协程 */ }
launch { /* 不受影响的协程 */ }
}
2. 全局异常处理器
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("Caught exception: $throwable")
}
// 应用于协程作用域
val scope = CoroutineScope(Dispatchers.IO + exceptionHandler)
七、协程的最佳实践
1. 避免使用 GlobalScope
// 错误做法
GlobalScope.launch { /* ... */ }
// 正确做法
class MyClass {
private val scope = CoroutineScope(Dispatchers.IO + Job())
fun doWork() = scope.launch { /* ... */ }
fun cleanup() = scope.cancel() // 生命周期结束时取消
}
2. 选择合适的调度器
// UI 操作在主线程
launch(Dispatchers.Main) {
textView.text = "Loading..."
val data = withContext(Dispatchers.IO) { fetchData() } // 切换到 IO 线程
textView.text = data
}
3. 避免协程嵌套
// 错误做法:嵌套协程
launch {
launch { /* 子协程 */ }
}
// 正确做法:使用 async 组合结果
val result = coroutineScope {
val deferred1 = async { task1() }
val deferred2 = async { task2() }
deferred1.await() + deferred2.await()
}
4. 处理背压(Backpressure)
当生产者速度快于消费者时,使用 Channel
或 Flow
:
// 使用 buffer 处理背压
flow {
for (i in 1..1000) emit(i)
}.buffer(100) // 缓冲 100 个元素
.collect { /* 处理元素 */ }
5. 资源管理
// 使用 use 自动关闭资源
withContext(Dispatchers.IO) {
File("data.txt").useLines { lines ->
lines.forEach { println(it) }
}
}
八、协程的性能优化
减少协程创建开销:
- 避免在循环中创建大量协程,使用
map
+awaitAll
批量处理。
val results = list.map { async { process(it) } }.awaitAll()
- 避免在循环中创建大量协程,使用
复用调度器:
- 避免频繁创建新的
SingleThreadContext
,使用共享实例。
- 避免频繁创建新的
监控协程泄漏:
- 使用
DebugProbes
(测试环境)检测未完成的协程。
// 在测试中使用 DebugProbes.install() // 测试结束后检查 DebugProbes.assertNoActiveCoroutines()
- 使用
九、常见陷阱与注意事项
阻塞 vs 挂起:
- 避免在协程中使用
Thread.sleep()
(阻塞线程),应使用delay()
(挂起协程)。
- 避免在协程中使用
线程安全:
- 协程不保证线程安全,共享可变状态时需同步(如使用
Mutex
)。
- 协程不保证线程安全,共享可变状态时需同步(如使用
内存泄漏:
- 长生命周期协程引用短生命周期对象(如 Activity)时需谨慎。
测试协程代码:
- 使用
TestCoroutineDispatcher
控制协程执行:
@Test fun testCoroutine() = runBlockingTest { // 测试协程逻辑 }
- 使用
十、协程的应用场景
异步 I/O 操作:
- 网络请求、文件读写等。
UI 响应性优化:
- 将耗时操作放在后台协程,保持 UI 流畅。
并发任务处理:
- 并行执行多个独立任务,合并结果。
数据流处理:
- 使用
Flow
处理异步数据流。
- 使用
状态机实现:
- 通过协程实现复杂的状态流转逻辑。
总结
Kotlin 协程通过轻量级、非阻塞的特性,彻底改变了异步编程的体验。其核心优势在于:
- 高效资源利用:减少线程创建开销,提升系统吞吐量。
- 简洁代码结构:使用同步写法实现异步逻辑,避免回调地狱。
- 安全的并发模型:通过结构化并发和作用域管理,降低内存泄漏风险。
掌握协程需要理解其核心概念(作用域、调度器、挂起函数)和最佳实践,避免常见陷阱。在实际项目中,协程特别适合处理 I/O 密集型任务和需要高响应性的应用场景。