第2章 协程构建器与调度器实战:launch、async、runBlocking、Dispatchers.IO/Main/Default
2.1 核心概念与运行模型
协程构建器是启动协程的唯一入口,调度器则决定协程在哪个线程池或线程上执行。Kotlin 提供三个最常用的构建器:launch(无返回值)、async(有返回值,需调用 await() 获取)和 runBlocking(阻塞当前线程以启动协程,主要用于桥接非协程代码)。调度器 Dispatchers.IO、Dispatchers.Main 和 Dispatchers.Default 分别对应 I/O 密集型、UI 线程和 CPU 密集型任务。选择合适的构建器与调度器组合,是协程正确运行和性能优化的基础。
2.2 构建器详解与实现流程
| 构建器 | 返回类型 | 典型用法 | 异常传播 | 适用场景 |
|---|---|---|---|---|
launch | Job | 启动后不关心结果 | 自动向父协程传播 | 发请求、写日志、UI更新 |
async | Deferred<T> | 需要结果时调用 .await() | 调用 await() 时抛出 | 并发获取多个数据源 |
runBlocking | T(直接返回) | 阻塞当前线程直到内部完成 | 立即抛出(同步风格) | main函数、测试、桥接回调 |
实现流程口诀: 先创建 CoroutineScope,再选择构建器,指定调度器,在代码块内执行挂起函数。Android 中通常用
lifecycleScope 或 viewModelScope 自动管理生命周期。
2.3 调度器选择策略
- Dispatchers.IO:底层线程池可扩展,最大线程数默认 64(Kotlin 1.4+),适合阻塞式 I/O 操作(文件读写、网络请求)。
- Dispatchers.Main:在 Android 上等于主线程,用于更新 UI、LiveData 赋值、界面交互。在主线程执行耗时操作会导致 ANR。
- Dispatchers.Default:线程数 = CPU 核心数(最小 2),用于计算密集型任务,如列表排序、数据解析。
性能建议: 避免在 Dispatchers.Default 中执行网络请求(会占用 CPU 线程),也不要在 Dispatchers.IO 中执行大量 CPU 计算(线程切换开销大)。使用
withContext(Dispatchers.IO) { ... } 在协程内切换调度器。
2.4 代码示例:组合实战
以下示例展示在 ViewModel 中同时从网络和数据库获取数据,使用 async + Dispatchers.IO 并发执行,最终合并结果。
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val userDeferred = async(Dispatchers.IO) { fetchUserFromNetwork() }
val postsDeferred = async(Dispatchers.IO) { fetchPostsFromDatabase() }
// 等待两个结果
val user = userDeferred.await()
val posts = postsDeferred.await()
// 在主线程更新 UI
withContext(Dispatchers.Main) {
_uiState.value = UiState.Success(user, posts)
}
}
}
private suspend fun fetchUserFromNetwork(): User { /* 模拟网络请求 */ }
private suspend fun fetchPostsFromDatabase(): List<Post> { /* 模拟数据库读取 */ }
}
2.5 常见错误与调试
| 错误模式 | 现象 | 解决方案 |
|---|---|---|
在 launch 中忘记指定 IO 调度器 | 网络请求阻塞 Default 线程池,导致 UI 卡顿 | 显式指定 launch(Dispatchers.IO) { } |
在 runBlocking 内部调用 launch 未使用 CoroutineScope | 协程执行顺序混乱或内存泄漏 | 直接使用 runBlocking { launch { ... } } 会自动创建顶层协程 |
在 async 的 Deferred 上无限等待 await() | 死锁或无响应 | 确保协程能完成,避免在 Dispatchers.Main 上执行耗时操作 |
| 未捕获的异常导致协程取消 | 无法获取错误信息 | 使用 CoroutineExceptionHandler 或在 launch 块内 try-catch |
2.6 性能与稳定性优化
- 限制并发数: 使用
Dispatchers.IO时,可通过newFixedThreadPoolContext或limitedParallelism(Kotlin 1.6+)控制最大线程数,避免打满线程池。 - 避免
runBlocking在 UI 线程使用: 仅在测试或 main 函数中使用,否则会阻塞主线程导致 ANR。 - 使用
SupervisorJob: 当某个子协程失败时不影响兄弟协程,适用于独立任务(如多个独立网络请求)。
// 使用 limitedParallelism 控制 IO 并发数
val limitedIO = Dispatchers.IO.limitedParallelism(4)
viewModelScope.launch(limitedIO) {
// 最多同时执行 4 个 I/O 任务
}
警告:
runBlocking 在 Android 的生产代码中几乎总是反模式。如果必须在 Activity 或 Fragment 中启动协程,请使用 lifecycleScope.launch 或 GlobalScope.launch(谨慎使用)。切勿在 onCreate 中直接调用 runBlocking。
2.7 调度器与线程调试技巧
在协程内部打印当前线程名有助于验证调度器是否正确:
lifecycleScope.launch {
println("启动线程: ${Thread.currentThread().name}") // 通常为 main
withContext(Dispatchers.IO) {
println("IO 线程: ${Thread.currentThread().name}") // 类似 DefaultDispatcher-worker-1
}
withContext(Dispatchers.Default) {
println("Default 线程: ${Thread.currentThread().name}") // 类似 DefaultDispatcher-worker-2
}
}
Android Studio 的 Coroutines Debugger 可以可视化协程的状态和挂起点,在调试时务必开启(Run → Debug with Coroutines)。