runBlocking — Kotlin 中的 Coroutine Builder,它会阻塞当前线程直到传递的协程完成。与 launch 和 async 不同,它不是 suspend 函数,可以从普通的(阻塞)代码中调用。根据 JetBrains 文档,2024,runBlocking 充当同步和异步世界之间的桥梁,允许从 main 函数和测试中启动协程。
要点
runBlocking — 是一个 Kotlin 函数,它创建一个新的 CoroutineScope 并启动传递的协程,阻塞当前线程直到其完全完成。与所有其他 Coroutine Builder 不同,runBlocking 不是 suspend 函数,可以从普通的同步代码中调用。runBlocking 的签名接受 CoroutineContext 和 suspend 块,返回类型为 T 的结果。
public fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
runBlocking 在当前线程中启动一个新的事件循环(event-loop)。当协程调用 suspend 函数(例如 delay() 或 await())时,runBlocking 会阻塞线程并在同一线程中执行其他已调度的协程,直到暂停的协程恢复。这是协作式阻塞 —— 线程不会空闲,而是处理其他协程。
runBlocking 的内部机制基于事件循环:当调用 suspend 函数时,runBlocking 暂停当前块的执行并从队列中启动其他协程。当 suspend 函数完成时,执行恢复。这个循环持续到所有协程完成。
runBlocking 使用自己的单线程池来执行协程。与 Dispatchers.IO 或 Default 不同,runBlocking 不会切换线程 —— 它在当前线程中处理所有协程,交错执行它们。这是唯一一个保证在同一线程中执行的构建器。
fun main() {
val threadName = Thread.currentThread().getName()
println("在 runBlocking 之前在线程 $threadName")
val result = runBlocking {
println("在 runBlocking 内部在线程 ${Thread.currentThread().getName()}")
delay(500L)
"Done"
}
println("在 runBlocking 之后:$result")
}
输出将显示所有三个 println 都在一个线程上执行。runBlocking 不切换线程,而是通过事件循环在一个线程内组织协作式多任务处理。
runBlocking 在三种场景下是合理的:控制台应用程序中的 main() 入口点、suspend 函数的单元测试以及桥接 —— 从基于回调或阻塞的库中调用 suspend 代码。在 Android 生产代码中,在主线程上使用严格禁止。
| 场景 | 适用性 | 风险 |
|---|---|---|
| main() 控制台应用程序 | 是 | 无 —— 这是入口点,线程不会阻塞 UI |
| JUnit 测试 | 是 | 最小 —— 测试本质上是同步的 |
| Android UI 线程 | 否 | ANR、延迟、界面冻结 |
| 回调 → 协程 | 是,谨慎使用 | 长时间操作时线程池阻塞 |
对于 Android 测试,使用 kotlinx-coroutines-test 的 TestDispatcher 代替 runBlocking。这样可以控制时间、自动重置和测试隔离。
在大多数场景中,runBlocking 可以而且应该被异步替代方案取代。对于 Android,这些是 viewModelScope、lifecycleScope 或带有正确调度器的 CoroutineScope。对于测试 —— TestCoroutineDispatcher 和 runTest。
// 错误:在 Android 主线程上使用 runBlocking
runBlocking(Dispatchers.Main) {
val result = networkApi.fetchData()
textView.setText(result)
}
// 正确:lifecycleScope
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { networkApi.fetchData() }
textView.setText(result)
}
测试的替代方案是来自 kotlinx-coroutines-test 的 runTest。它创建一个带有虚拟时间的 TestCoroutineScope,允许测试延迟而无需实际等待。这加速了测试并使它们具有确定性。
最常见的场景 —— 测试 suspend 函数。测试中的 runBlocking 允许同步等待协程结果而无需更改架构。第二个场景 —— 具有回调 API 的库,其中 suspend 函数通过 runBlocking 从阻塞上下文中调用。
// 使用 runBlocking 测试 suspend 函数
class RepositoryTest {
@Test
fun `fetchUser returns correct data`() {
val repository = UserRepository(FakeApi())
val result = runBlocking {
repository.fetchUser("123")
}
assertEquals("John", result.name)
assertEquals("john@test.com", result.email)
}
}
为了桥接回调和 suspend 世界,使用 CompletableDeferred 与 runBlocking 组合代替回调 —— 这简化了异步操作链并提高了代码的可读性。
错误使用 runBlocking 是从阻塞方法迁移到协程时常见的错误之一。主要问题:在 Android Main-Thread 上调用、嵌套 runBlocking、在异步函数内部使用以及通过 runBlocking 启动长时间运行的操作。
黄金法则:runBlocking 是桥梁,不是替代品。仅用于连接阻塞和非阻塞世界。对于所有其他任务,使用 launch、async 或 lifecycleScope。
常见问题
runBlocking 是唯一一个不是 suspend 函数的构建器。它在当前线程中启动一个事件循环,并且直到所有协程完成才返回控制权。launch 和 async 立即返回控制权,在后台执行协程。
不推荐。ViewModel 有内置的 viewModelScope,它会自动管理协程并在销毁时取消它们。ViewModel 中的 runBlocking 会阻塞线程并且不会响应生命周期取消。
使用来自 kotlinx-coroutines-test 库的 runTest。它提供了带有虚拟时间控制、自动取消和确定性执行的 TestCoroutineScope。
事件循环 —— runBlocking 内部的事件处理循环。当协程被暂停时(例如 delay()),事件循环切换到在同一线程中执行其他就绪的协程。这在不切换线程的情况下创建了多任务处理的假象。
单个线程中的嵌套 runBlocking 会创建死锁 —— 外部块等待内部块,但内部块在外部块完成之前无法开始。在不同线程中这是可以接受的,但由于调试困难,强烈不推荐。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。