协程(Coroutine)详解
协程是一种轻量级的线程管理方案,它允许你以看似同步的方式编写异步代码,同时提供了更高效、更可控的并发处理能力。
核心概念
-
轻量级线程:
- 协程不是操作系统线程,而是用户态"虚拟线程"
- 一个线程可以运行数千个协程
- 协程切换开销远小于线程切换
-
挂起与恢复:
- 协程可以在不阻塞线程的情况下挂起(suspend)执行
- 当资源就绪时再恢复(resume)执行
- 挂起时释放底层线程供其他协程使用
-
结构化并发:
- 协程之间存在父子关系
- 父协程取消时会自动取消所有子协程
- 提供更好的资源管理和错误传播机制
与线程的对比
特性 | 线程 | 协程 |
---|---|---|
创建数量 | 数百个就会耗尽资源 | 可创建数十万个 |
切换开销 | 需要内核介入,开销大 | 用户态切换,开销极小 |
阻塞方式 | 阻塞整个线程 | 仅挂起当前协程 |
内存占用 | 每个线程需要MB级栈内存 | 每个协程仅需KB级内存 |
调度方式 | 由操作系统调度 | 由程序控制调度 |
协程的基本组成
-
挂起函数(Suspend Function):
suspend fun fetchData(): Data { // 可以调用其他挂起函数 return withContext(Dispatchers.IO) { // IO操作 } }
-
协程构建器:
launch
:启动不返回结果的协程async
:启动可返回结果的协程(通过Deferred)runBlocking
:阻塞当前线程直到协程完成(主要用于测试)
-
协程上下文:
Dispatchers.Main
:Android主线程Dispatchers.IO
:适合IO密集型操作Dispatchers.Default
:适合CPU密集型计算Job
:控制协程生命周期CoroutineName
:为协程命名便于调试
协程在Android中的优势
-
简化异步代码:
// 传统回调方式 api.getUser { user -> api.getPosts(user.id) { posts -> runOnUiThread { showUserData(user, posts) } } } // 协程方式 lifecycleScope.launch { val user = api.getUserSuspend() val posts = api.getPostsSuspend(user.id) showUserData(user, posts) // 自动在主线程执行 }
-
避免回调地狱:
- 用顺序代码写异步逻辑
- 异常处理更直观(try-catch)
-
生命周期感知:
viewModelScope
/lifecycleScope
自动取消- 避免内存泄漏
-
灵活的并发控制:
// 并发执行多个请求 val user = async { getUser() } val posts = async { getPosts() } showData(user.await(), posts.await())
典型应用场景
-
网络请求:
viewModelScope.launch { try { _state.value = LoadingState _state.value = SuccessState(api.fetchData()) } catch (e: Exception) { _state.value = ErrorState(e) } }
-
数据库操作:
suspend fun updateUser(user: User) { withContext(Dispatchers.IO) { db.userDao().insert(user) } }
-
多任务并行:
suspend fun fetchDashboardData() { coroutineScope { val deferredNews = async { newsRepo.getNews() } val deferredWeather = async { weatherRepo.getWeather() } val dashboard = Dashboard( news = deferredNews.await(), weather = deferredWeather.await() ) _dashboardData.value = dashboard } }
-
超时控制:
try { val result = withTimeout(3000) { fetchSlowData() } } catch (e: TimeoutCancellationException) { showTimeoutError() }
协程通过提供更高级的抽象,让开发者能够以更直观的方式处理并发和异步操作,同时获得更好的性能和更少的资源消耗。它是现代Android开发中处理异步任务的首选方案。
理解"协程可以在不阻塞线程的情况下挂起(suspend)执行"
这句话是协程最核心的特性之一,也是协程区别于传统线程的关键优势。让我通过几个角度来解释这个重要概念:
1. 基本概念解析
挂起(suspend):协程可以在执行到某个点时暂停自己,保存当前状态,释放底层线程资源
不阻塞线程:当协程挂起时,它所在的线程可以自由地去执行其他任务,不会被占用等待
2. 与传统线程阻塞的对比
传统线程模型(阻塞式):
// 伪代码
void fetchData() {
Thread.sleep(1000); // 线程被完全阻塞1秒钟
updateUI();
}
- 线程在等待期间完全不能做其他工作
- 大量线程等待会导致系统资源紧张
协程模型(非阻塞挂起):
suspend fun fetchData() {
delay(1000) // 协程挂起,线程可执行其他任务
updateUI() // 恢复后继续执行
}
- 协程"暂停"但线程空闲
- 一个线程可以交替执行多个协程的任务
3. 挂起机制的工作原理
协程挂起时会发生以下事情:
- 保存执行状态:将当前执行点、局部变量等保存在continuation对象中
- 释放线程:让出线程使用权,线程可以执行其他协程或任务
- 安排恢复:当挂起条件满足(如网络响应返回),协程会被重新调度执行
- 恢复执行:从之前保存的状态点继续执行,就像从未中断过
4. 实际执行流程示例
假设一个线程上有两个协程:
时间线:
0ms: 协程A开始执行 → 发起网络请求 → 挂起
线程空闲 → 切换到协程B
10ms: 协程B开始执行 → 发起数据库查询 → 挂起
线程空闲
50ms: 网络响应到达 → 恢复协程A
120ms: 数据库结果到达 → 恢复协程B
5. 为什么这很重要
- 资源高效:少量线程即可服务大量协程
- 响应迅速:没有线程被无谓地阻塞等待
- 简化代码:可以用看似同步的方式写异步逻辑
- 避免回调地狱:不需要嵌套回调处理异步结果
6. 技术实现的关键
- 状态保存:通过Continuation Passing Style (CPS)变换保存执行状态
- 调度器:决定哪个协程在哪个线程执行
- 挂起点:只有调用其他suspend函数时才能挂起
7. 具体代码示例分析
// 主线程
lifecycleScope.launch {
// 1. 在主线程开始执行
showLoading()
// 2. 遇到挂起函数,协程挂起,主线程空闲
val data = fetchFromNetwork()
// 4. 恢复后在主线程继续
updateUI(data)
}
suspend fun fetchFromNetwork(): Data {
// 3. 在IO线程执行网络请求
return withContext(Dispatchers.IO) {
// 模拟网络请求
delay(1000)
Data("response")
}
}
执行流程:
- 主线程开始执行协程代码
- 遇到
fetchFromNetwork()
挂起函数,协程挂起,主线程释放 - IO线程池中的某个线程执行实际网络请求
- 请求完成后,协程被重新调度到主线程恢复执行
8. 与回调方式的对比
传统回调方式:
fun fetchData(callback: (Data) -> Unit) {
thread {
// 阻塞线程
val data = blockingNetworkCall()
runOnUiThread {
callback(data)
}
}
}
协程方式:
suspend fun fetchData(): Data {
return withContext(Dispatchers.IO) {
// 非阻塞挂起
nonBlockingNetworkCall()
}
}
// 使用时可以直接顺序调用
val data = fetchData()
updateUI(data)
这种"不阻塞线程的挂起"机制使得协程能够以极低的资源开销实现高效的并发编程,特别是在I/O密集型应用中优势尤为明显。