suspend 不是后台线程
它只表示函数可以暂停并在稍后恢复;到底在哪个线程执行,取决于协程上下文和函数内部。
下面函数从 viewModelScope.launch 调用,会不会自动离开主线程?
suspend fun hash(bytes: ByteArray): String =
sha256(bytes) // 同步 CPU 计算- 会,所有 suspend 函数都在后台
- 会,因为返回值稍后才得到
- 不会;没有挂起点,也没有切换 Dispatcher
- 取决于函数名
暂停的是计算,不是线程
编译器会把 suspend 函数改写成一个能保存「执行到哪里、局部变量是什么」的状态机,并额外接收 Continuation。遇到真正挂起的调用,它先把状态保存后返回,让当前线程去做别的;结果到达时,再从下一个状态恢复。
因此 delay 不占着线程睡觉,而 Thread.sleep 会。一个 suspend 函数也可以从头到尾不挂起,照样把调用线程卡住。suspend 的承诺是「可暂停」,不是「并行」「后台」或「更快」。
Dispatcher 是执行政策
Dispatchers.Main 面向 UI;IO 面向会阻塞线程的网络、文件与数据库调用;Default 面向吃 CPU 的计算。它们不是性能咒语:现代 Retrofit 的 suspend 适配与 Room suspend DAO 已经是主线程安全的,没必要在调用处再套一层 withContext(IO)。
更好的责任划分是:底层函数对自己的阻塞负责。Repository 暴露主线程安全的 suspend API;ViewModel 直接调用,不必知道底下是同步 SDK、Room 还是 Retrofit。若某个旧 SDK 会阻塞,由包装它的 data source 切 Dispatcher。
class AttachmentHasher(
private val cpu: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun hash(bytes: ByteArray): String = withContext(cpu) {
sha256(bytes)
}
}
注入 Dispatcher 的价值主要是测试:生产用真实线程池,测试用受控调度器。不要把 Dispatchers.IO 散落在每个调用者里,否则线程政策既重复又难替换。
launch 与 async 不是「要不要并发」
launch 返回 Job,适合「只关心完成或取消」的工作;async 返回 Deferred<T>,适合有结果、而且确实要与别的工作并发。顺序执行两个 suspend 函数时不需要 async。并发也只在任务独立时成立,第二步依赖第一步结果就不能硬并。
同一条线程先经历非阻塞延迟,再经历阻塞睡眠;输出顺序相同,但后者占住线程:
A before delay B while A is suspended A after delay C before sleep C after sleep D could not run during sleep
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
println("A before delay")
delay(50)
println("A after delay")
}
launch { println("B while A is suspended") }
delay(80)
launch {
println("C before sleep")
Thread.sleep(50)
println("C after sleep")
}
launch { println("D could not run during sleep") }
}在 Android Studio Kotlin/JVM scratch 中运行,并加入 kotlinx-coroutines-core。再给第二组 launch(Dispatchers.Default),观察「阻塞线程」和「阻塞整个单线程上下文」的区别。
给临床照片算哈希、压缩或做图像处理属于 CPU 工作;读取内容 URI 可能阻塞;上传若用现代异步客户端则可挂起而不占线程。三者都叫「附件处理」,调度策略却不同。用职责封装它们,UI 只看到一个主线程安全的用例。
「函数标了 suspend,就不可能造成 ANR。」同步循环、阻塞 I/O、锁等待都能在 suspend 函数里卡住主线程。
suspend 描述控制流能力;Dispatcher 描述执行位置;库的实现决定是否阻塞。三者分别判断。
答案是第 3 项。sha256 是同步 CPU 计算,函数没有挂起点,也没有切到 Default,所以会在调用它的主线程执行。正确修复在拥有这段阻塞工作的底层封装里切换 Dispatcher,并让上层 API 保持主线程安全。
这一章的一句话
suspend 决定能不能停下来,Dispatcher 决定在哪里继续,二者都不保证代码本身不阻塞。
下一章把单个协程变成一棵树:一个附件上传失败时,另外两个要不要取消?两种答案只差一个 supervisorScope,业务语义却完全相反。