Kotlin 协程技术文档
简介
Kotlin 协程是 Kotlin 官方提供的一种用于简化异步编程和并发任务处理的工具。它能够让你用同步的写法实现异步代码,大大简化了回调地狱(callback hell)问题,提高了代码的可读性和维护性。协程在 Android 开发、服务器开发等场景中都有广泛应用。
协程基本概念
什么是协程
协程是一种轻量级的线程,能够在单个线程内并发执行多个任务。它基于挂起(suspending)和恢复(resuming)机制,在任务遇到耗时操作(如 IO、网络请求)时挂起执行,不阻塞线程,待条件满足后恢复执行。
协程与线程的对比
- 轻量性:协程比线程更加轻量,一个应用可以同时启动成千上万个协程,而线程数量通常受限于系统资源。
- 调度模型:协程由调度器(Dispatchers)管理,可以在多个线程间灵活调度,而线程调度依赖于操作系统。
- 切换成本:协程的上下文切换成本远低于线程切换,性能开销较小。
- 编程模型:协程可以用顺序化的代码编写异步逻辑,避免回调嵌套,使代码更直观。
协程构建块
CoroutineScope
- 定义 :
CoroutineScope
表示一个协程作用域,它限定了协程的生命周期。所有在该作用域内启动的协程都遵循同一个生命周期,便于集中管理。 - 创建 :可以通过
GlobalScope
(不推荐使用)、CoroutineScope(Job())
或 Android 的viewModelScope
、lifecycleScope
来创建作用域。
示例:
scss
// 自定义 CoroutineScope
val myScope = CoroutineScope(Dispatchers.Main + Job())
启动协程:launch 与 async
-
launch 用于启动一个不需要返回结果的协程,返回一个
Job
对象。scssmyScope.launch { // 在协程中执行耗时操作 delay(1000) println("Hello from launch!") }
-
async 用于启动一个协程并返回一个
Deferred
对象,表示未来可能返回的结果。常用于并行计算。kotlinval 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 不受限于特定线程,立即在当前线程中执行,通常用于测试或特殊场景。
示例:
scss
// 在 IO 线程中执行网络请求
CoroutineScope(Dispatchers.IO).launch {
val data = fetchDataFromNetwork()
// 切换回主线程更新 UI
withContext(Dispatchers.Main) {
updateUI(data)
}
}
自定义调度器
可以创建自定义线程池,并用 asCoroutineDispatcher()
方法转换为协程调度器:
ini
val myExecutor = Executors.newFixedThreadPool(4)
val myDispatcher = myExecutor.asCoroutineDispatcher()
withContext()
的使用
withContext()
是 Kotlin 协程中的 挂起函数 ,用于 在协程中切换执行的调度器。它不会创建新的协程,而是挂起当前协程并在指定的调度器上执行代码块,执行完成后恢复到原来的调度器。
withContext()
会挂起外部协程,直到内部代码执行完毕,才继续执行外部协程的后续代码 ,但不会阻塞线程。
基本用法
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start: ${Thread.currentThread().name}")
withContext(Dispatchers.IO) {
println("Switch to IO: ${Thread.currentThread().name}")
delay(1000) // 模拟耗时操作
}
println("Back to main: ${Thread.currentThread().name}")
}
执行过程
runBlocking
在 主线程 中运行。withContext(Dispatchers.IO)
切换到 IO 线程池 并执行代码块。delay(1000)
让协程挂起 1 秒,但不会阻塞线程。withContext
执行完毕,恢复到 原来的调度器(主线程) 。
常见用途
1. 在 Dispatchers.Main
执行 UI 更新
适用于 Android 开发 ,用于 在后台线程执行任务,并回到主线程更新 UI。
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) { fetchData() }
textView.text = data // 回到主线程更新 UI
}
}
private suspend fun fetchData(): String {
delay(2000) // 模拟网络请求
return "Data from server"
}
}
🔹 fetchData()
在 IO 线程执行 ,获取数据后 withContext
自动 回到主线程 更新 textView
。
2. 计算密集型任务 (Dispatchers.Default
)
适用于 CPU 密集型计算(如排序、加密、数据处理)。
kotlin
suspend fun heavyComputation() {
withContext(Dispatchers.Default) {
val result = (1..1_000_000).sum()
println("Computation result: $result")
}
}
🔹 计算任务会在 默认线程池 (Default
)运行,不会阻塞 UI。
3. 读取本地文件(IO 线程)
kotlin
suspend fun readFile(): String {
return withContext(Dispatchers.IO) {
File("data.txt").readText()
}
}
🔹 避免主线程阻塞 ,文件读取在 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
内,一个子协程的异常不会传播到其他子协程,可以实现部分任务失败而不影响整体流程。inisupervisorScope { val job1 = launch { // 子任务 1 } val job2 = launch { // 子任务 2 } }
协程取消与超时控制
取消协程
协程取消是一种协作机制,被取消的协程需要在合适的地方检测取消状态。常用方法包括:
- 调用
job.cancel()
:取消当前 Job 以及其所有子协程。 - 挂起点检测 :例如
delay()
、withContext()
都会检查取消状态。
示例:
kotlin
val job = myScope.launch {
repeat(1000) { i ->
if (!isActive) return@launch // 检查协程是否被取消
println("Processing $i")
delay(100)
}
}
// 取消协程
job.cancel()
withTimeout 与 withTimeoutOrNull
-
withTimeout 规定一段时间内未完成任务,则抛出
TimeoutCancellationException
。scsstry { withTimeout(3000) { // 如果 3 秒内没有完成,则异常退出 performLongRunningTask() } } catch (e: TimeoutCancellationException) { println("任务超时") }
-
withTimeoutOrNull 超时返回
null
,不会抛异常。scssval result = withTimeoutOrNull(3000) { performLongRunningTask() } if (result == null) { println("任务超时") }
异常处理
CoroutineExceptionHandler
用于全局捕获协程未处理的异常。通过在协程上下文中添加 CoroutineExceptionHandler
,可以统一处理异常。
javascript
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获到异常:${exception.message}")
}
myScope.launch(handler) {
throw RuntimeException("测试异常")
}
try/catch 在协程中的使用
在挂起函数内部,同步代码中的 try/catch
同样适用,用于捕获特定的异常。
kotlin
myScope.launch {
try {
val result = performRiskOperation()
println("结果:$result")
} catch (e: Exception) {
println("错误:${e.message}")
}
}
协程通信与同步
Channel
Channel 提供了一种在协程之间进行通信的方式,类似于阻塞队列。通过 Channel,可以实现生产者-消费者模型。
示例:
scss
import kotlinx.coroutines.channels.Channel
val channel = Channel<Int>()
// 生产者协程
launch {
for (i in 1..5) {
channel.send(i)
}
channel.close() // 关闭 Channel
}
// 消费者协程
launch {
for (number in channel) {
println("接收数据: $number")
}
}
Flow
Flow 是 Kotlin 协程中的响应式流,用于处理异步数据流和背压控制。
- 定义 Flow :使用
flow {}
构建数据流 - 收集数据 :使用
collect {}
处理数据
scss
import kotlinx.coroutines.flow.*
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i) // 发射数据
}
}
launch {
simpleFlow().collect { value ->
println("收到:$value")
}
}
与 Android 集成
在 Android 开发中,协程可以很好地解决异步任务和 UI 更新问题。常见的集成方式包括:
在 UI 线程中启动协程
利用 Dispatchers.Main
在主线程中更新 UI:
scss
CoroutineScope(Dispatchers.Main).launch {
val data = withContext(Dispatchers.IO) { fetchDataFromNetwork() }
textView.text = data
}
使用 ViewModelScope 和 LifecycleScope
-
viewModelScope 适用于 ViewModel 中启动协程,与 ViewModel 生命周期绑定,避免内存泄漏。
kotlinclass MyViewModel : ViewModel() { init { viewModelScope.launch { // 执行网络请求或数据加载 } } }
-
lifecycleScope 适用于 Activity 或 Fragment,自动管理协程生命周期。
kotlinclass 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。