第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 密集型任务。选择合适的构建器与调度器组合,是协程正确运行和性能优化的基础。

协程构建器与调度器协作流程图 launch 返回 Job,无结果值 async 返回 Deferred<T>,可 await runBlocking 阻塞当前线程,桥接用 Dispatchers.IO I/O 密集型(网络/文件) Dispatchers.Main UI 线程(Android 主线程) Dispatchers.Default CPU 密集型(计算/排序) 每个构建器可指定调度器:launch(Dispatchers.IO) { ... } 未指定时默认使用继承的调度器(一般为 Dispatchers.Default)

2.2 构建器详解与实现流程

构建器返回类型典型用法异常传播适用场景
launchJob启动后不关心结果自动向父协程传播发请求、写日志、UI更新
asyncDeferred<T>需要结果时调用 .await()调用 await() 时抛出并发获取多个数据源
runBlockingT(直接返回)阻塞当前线程直到内部完成立即抛出(同步风格)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)。