调度器 —— 协程到底在哪个线程上跑?

协程不绑定任何线程——它是线程的过客,Dispatcher 是派工系统

Dispatcher Main/IO/Default/Unconfined withContext 线程切换

目录导航(点击跳转)

一、先搞清楚:协程 ≠ 线程

1三个角色的关系
概念角色说明
协程工单你要干的活——一段代码 + 一个状态机对象
线程工人干活的人——OS 内核管理的执行单元
Dispatcher派工系统把工单分给哪个工人——决定协程跑在哪个线程上
launch {                    // 这是一个协程(工单)
    println("干活")          // 这个活
}                           // 由调度器决定交给哪个线程执行

协程本身不绑定任何线程。它只是一段代码 + 一个状态机对象。谁跑它,由 Dispatcher 说了算。

二、四种 Dispatcher

1总览
Main
UI 线程
1 个
IO
I/O 线程池
64 个
Default
CPU 线程池
核数个
Unconfined
无固定线程
抓到谁是谁
2① Dispatchers.Main —— UI 线程
launch(Dispatchers.Main) {
    // 在这里更新 UI
    textView.text = "hello"
}
3② Dispatchers.IO —— I/O 线程池
launch(Dispatchers.IO) {
    val data = httpClient.get("https://...")  // 在 IO 线程上跑
    withContext(Dispatchers.Main) {
        textView.text = data                  // 切回主线程更新 UI
    }
}
4③ Dispatchers.Default —— CPU 线程池
launch(Dispatchers.Default) {
    // 排序大数组、解析 JSON、加密...
    val sorted = bigList.sorted()
}
5④ Dispatchers.Unconfined —— 不指定
launch(Dispatchers.Unconfined) {
    println("在调用者的线程上开始")
    delay(100)
    println("在恢复线程上继续(可能是另一个)")
}
6四种 Dispatcher 对比
Dispatcher线程池大小适用场景典型操作
Main1 个(UI 线程)UI 更新setText()、更新 LiveData/StateFlow
IO64 个(按需扩缩)网络、文件HTTP 请求、文件读写、数据库
DefaultCPU 核数个计算密集排序、加解密、JSON 解析
Unconfined无固定特殊场景测试、不关心线程的轻量操作

三、挂起后恢复,可能在完全不同的线程上

1代码演示
launch(Dispatchers.IO) {
    println("挂起前:${Thread.currentThread().name}")   // IO-thread-1

    delay(1000)                                         // 挂起!
    // 挂起期间,IO-thread-1 去跑别的协程了

    println("恢复后:${Thread.currentThread().name}")   // IO-thread-3(可能不同!)
}
2协程A的一生
协程A IO-thread-1 IO-thread-3
1创建 → 进入 Dispatcher 就绪队列
2IO-thread-1 拿到它 → 执行
delay(1000) → 挂起 → 续体进定时器队列 → 离开所有线程
3IO-thread-1 去跑别的协程
1秒到 → Dispatcher 找空闲线程 → IO-thread-3 空闲
4IO-thread-3 恢复协程A → 从 delay 后面继续

协程从不占有线程。它是线程的过客。创建→Dispatcher队列→某线程执行→挂起→离开→恢复→可能换另一个线程→再挂起→再换……

四、withContext:临时换线程

1代码示例
launch(Dispatchers.Main) {                       // 主线程
    val result = withContext(Dispatchers.IO) {    // 切到 IO 线程
        heavyNetworkCall()                        // 在 IO 线程跑
    }  // ← 自动切回主线程
    textView.text = result                        // 回到主线程更新 UI
}
2线程切换图
Main IO
启动协程 → launch
withContext(IO) → heavyNetworkCall()
自动切回 → 更新 UI

withContext 做的事:挂起当前协程 → 在新线程跑代码 → 跑完把结果传回 → 在原线程恢复。不需要回调、不需要 Handler.post、不需要 runOnUiThread。

五、Dispatcher 全流程

1一张图看懂 Dispatcher
新协程:launch(Dispatchers.IO) { ... }
Dispatchers.IO
┌────────────┐            ┌──────────────┐
│ 任务队列    │──────→│ IO-thread-1 │
│ 新协程 ◆  │──────→│ IO-thread-2 │
│ 恢复协程    │──────→│ IO-thread-3 │
│ ...        │      │ ...          │
└────────────┘            └──────────────┘
                                线程池
新协程入队
有空闲线程
→ 执行
挂起
→ 线程回池
恢复
→ 回队列
epoll 通知
→ 重新调度

六、核心思想总结

第六章核心要点

一句话总结第六章

协程不绑定线程,它只是一段活在堆上的状态机代码。Dispatcher 决定它跑到哪个线程上:Main→UI、IO→网络文件、Default→CPU计算。挂起前在 Thread-1,恢复后可能在 Thread-3——完全透明。withContext 临时换线程,跑完自动回来,零回调。