runBlocking 详解:阻塞当前线程启动协程
runBlocking 这个名字挺直白的——运行并阻塞。说白了,它就是一个桥梁:一头连着普通的同步代码,另一头连着协程世界。你调用它的时候,当前线程就会被卡住,直到协程体跑完才放行。
关键签名
public actual fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
它干了什么?
我刚开始学协程时觉得这玩意儿很矛盾——协程不是要避免阻塞吗?后来才发现,runBlocking 的存在是为了解决协程和非协程代码之间的鸿沟。比如在 main 函数、JUnit 测试、Spring Boot 启动入口这些地方,你不能直接挂起,得有个人来“翻译”。
它会做三件事:
- 创建一个新的协程并立即启动它
- 阻塞当前线程,直到这个协程执行完毕
- 把协程的结果或异常原封不动地抛出来
注意了,这里的阻塞是真的阻塞,不是挂起。线程被占用,无法做其他任何事情。
工作流程(看一眼这张图就明白了)
什么时候该用它?
场景一:main 函数的入口
这是最常见的用法。Kotlin 的 main 函数不能是挂起函数,但你写的业务逻辑又是协程式的。那就套一层 runBlocking。
fun main() = runBlocking {
println("Hello from协程")
delay(1000)
println("1秒后")
}
我习惯在原型验证或写小脚本时这么用,干净利落。
场景二:JUnit 测试
在单元测试里测试挂起函数,runBlocking 几乎是标配。像这样:
@Test
fun testSuspend() = runBlocking {
val result = someSuspendFunction()
assertEquals(42, result)
}
不过,如果你的协程里还有 launch 或 async,那就得小心了——runBlocking 只等待最外层的协程体,内部启动的协程可能还没跑完就结束了。我曾经因为这个原因排查了一下午,测试明明没报错,但异步操作就是没执行。
注意:如果测试里有 async,记得在块末尾调用 .await() 等子协程完成。
场景三:框架启动时的临时阻塞
有些框架初始化时必须同步等待某个协程做完(比如加载配置、连接数据库)。这时候 runBlocking 能帮你强制序列化。但记住——这只是权宜之计,不能让这种写法渗透到业务代码里。
常见陷阱——我踩过的坑
陷阱一:在 Android 主线程用 runBlocking
这个我必须要强调。Android 的 UI 线程(主线程)如果被阻塞超过 5 秒就会 ANR 弹窗。你想想看,一次网络请求就算运气好也要几百毫秒,如果你在主线程里写:
// ❌ 千万不能这样
button.setOnClickListener {
runBlocking {
val data = networkCall() // 阻塞!ANR!
textView.text = data
}
}
结果是用户点按钮,然后 App 卡住,接着弹出“应用无响应”。遇到这种问题,解决方案很简单:用 lifecycleScope.launch 或 viewModelScope.launch,千万别在主线程上玩阻塞。
陷阱二:嵌套 runBlocking
有些人觉得一个 runBlocking 不够,又在里面套一层。这会导致线程再被阻塞一次,如果内部 runBlocking 跑在同一个线程上,直接死锁。我记得有次调试一个奇怪的超时,最后发现就是嵌套两层 runBlocking 搞的鬼。
// ❌ 嵌套有风险
runBlocking {
runBlocking { // 死锁可能
delay(100)
}
}
实际上,你几乎永远不需要嵌套 runBlocking。如果遇到需要,大概率是设计有问题,回头看看代码结构能不能重构。
陷阱三:在协程内部用 runBlocking 做“同步”
已经有协程上下文了,你却还想着用 runBlocking 来“等待”某个结果。这通常是还没理解 withContext 导致的。比如:
// ❌ 错误的写法
suspend fun fetchData(): String = withContext(Dispatchers.IO) {
runBlocking { // 多此一举
delay(500)
"data"
}
}
正确的做法是直接用 delay (挂起) 或者 withContext 切线程,根本用不到 runBlocking。
使用建议一览表
| 场景 | 建议 | 替代方案 |
|---|---|---|
| main 函数 | ✅ 推荐使用 | 无 |
| 单元测试 | ✅ 推荐使用,注意子协程 | kotlinx-coroutines-test |
| Android UI 线程 | ❌ 禁止使用 | lifecycleScope.launch |
| 已有协程上下文 | ❌ 不要嵌套 | withContext / async-await |
| 初始化时需要同步等待 | ⚡ 谨慎使用(一次性的) | runBlocking 可以,但别长期持有 |
小贴士:runBlocking 默认使用创建它的线程作为协程上下文。如果你不想阻塞当前线程,而是想切到某个特定的调度器,可以显式传 Dispatchers.IO 等参数。不过说实话,这种情况不如直接用 launch 或 async 来得自然。
最后说一句
runBlocking 是一把双刃剑。用对了,它能帮你在同步世界里优雅地调用协程;用错了,它就是性能杀手和 ANR 的温床。我个人经验是:只有在“必须用同步方式调用协程”且“只有一次”的情况下才掏它出来。其他时候,尽量用 structured concurrency 那一套——挂起、作用域、launch/async,它们才是协程的日常。