1. Kotlin协程简介
协程这个概念,说白了就是一套「轻量级的并发方案」。我第一次接触协程时也困惑过:这不就是线程封装吗?后来在项目中踩过坑才明白——它本质上是一种可挂起、可恢复的计算过程。
协程是什么?
Kotlin的协程是一种协作式多任务机制。它允许你在代码中暂停一个任务,然后稍后恢复。注意,这里的「暂停」不是阻塞线程。我习惯这样理解:协程运行在线程上,但挂起时它会释放线程,让线程去做其他事。等条件满足,再回来继续。
- 轻量级:一个线程可以运行成千上万个协程。
- 非阻塞:挂起时不会占用线程。
- 结构化并发:通过CoroutineScope管理生命周期。
CoroutineScope:协程的作用域
每个协程都必须在一个作用域内启动。CoroutineScope定义了协程的上下文和生命周期。
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
// 协程体
}
我个人习惯用viewModelScope或lifecycleScope来绑定界面生命周期。我曾经在写网络请求时忘记取消协程,导致界面销毁后还在回调,直接崩了。后来我严格限制作用域,再没出过问题。
Continuation:协程的“档位”
Continuation是协程内部的挂起恢复机制。每次遇到挂起函数,协程会把当前状态打包成一个Continuation对象保存起来。等条件就绪,就通过这个对象恢复执行。
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
你可能会问:这东西跟我写代码有什么关系?嗯,关系不大,但理解它有助于你排查堆栈里的奇怪调用。我看到过不少崩溃日志里出现Continuation,明白了原理就很容易定位。
协程 vs 线程
很多新手喜欢把协程当成“超级线程”。其实它们差别很大。我整理了一张对比表:
| 特性 | 协程 | 线程 |
|---|---|---|
| 启动成本 | 极低(栈在堆中) | 较高(分配栈空间) |
| 切换开销 | 用户态切换,几乎无开销 | 内核态上下文切换 |
| 阻塞 | 挂起不阻塞线程 | 阻塞会占用系统资源 |
| 调度 | 协作式,主动挂起 | 抢占式,由系统调度 |
| 并发数 | 轻松上万 | 数千即需谨慎 |
说实话,我刚开始用协程时,还是在用线程那套思维写代码:用delay替代Thread.sleep而已。后来遇到耗时的计算任务,直接在协程里写了个循环,结果卡住了整个线程。我才意识到:协程并不会自动把密集计算放到后台线程,你得配合withContext(Dispatchers.Default)才行。
核心观点:协程是线程的「管理者」,不是替代品。它让你用同步的写法处理异步逻辑。
协程启动的基本方式
Kotlin提供了launch和async两种启动方式:
// launch:返回Job,不关心结果
scope.launch {
delay(1000)
println("Done")
}
// async:返回Deferred,等待结果
val deferred = scope.async {
delay(1000)
"Result"
}
val result = deferred.await()
launch适合“发后不管”,async适合需要返回值的场景。我建议优先用launch,除非你明确需要并发拿结果。
小技巧:在Android的ViewModel里,用viewModelScope.launch就行,它会自动绑定ViewModel生命周期。
注意:不要在launch块里直接调用Thread.sleep,那会阻塞整个线程。要用delay,它是挂起函数。
这张图梳理了协程执行的核心流程。从CoroutineScope开始,经过launch/async启动,执行挂起函数时,Continuation会保存现场,之后可以选择恢复执行或直接完成。注意,挂起期间线程可以干别的事。
总结
这一章我们讲了协程的基本概念、CoroutineScope的作用、Continuation的底层机制,以及和线程的对比。协程不是银弹,但用对了地方,能极大简化并发代码。记住:结构化并发是Kotlin协程的精华,好好利用作用域,避免泄露。