runBlocking 详解:阻塞当前线程启动协程

runBlocking 这个名字挺直白的——运行并阻塞。说白了,它就是一个桥梁:一头连着普通的同步代码,另一头连着协程世界。你调用它的时候,当前线程就会被卡住,直到协程体跑完才放行。

关键签名

public actual fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

它干了什么?

我刚开始学协程时觉得这玩意儿很矛盾——协程不是要避免阻塞吗?后来才发现,runBlocking 的存在是为了解决协程和非协程代码之间的鸿沟。比如在 main 函数、JUnit 测试、Spring Boot 启动入口这些地方,你不能直接挂起,得有个人来“翻译”。

它会做三件事:

  1. 创建一个新的协程并立即启动它
  2. 阻塞当前线程,直到这个协程执行完毕
  3. 把协程的结果或异常原封不动地抛出来

注意了,这里的阻塞是真的阻塞,不是挂起。线程被占用,无法做其他任何事情。

工作流程(看一眼这张图就明白了)

runBlocking 核心流程 调用者线程 (main / 测试线程) 协程体 (挂起函数) runBlocking 创建协程 阻塞当前线程 调用 启动 结果/异常 返回 解除阻塞 ✅ 正确使用场景 • main 函数入口 • JUnit 测试 • 框架启动(非性能敏感处) ⚠️ 常见陷阱 • 在 Android UI 线程中使用 • 嵌套多个 runBlocking • 长时间阻塞导致线程饿死 💡 本质:同步世界与异步协程之间的胶水 使用时问自己:这个阻塞真的无法避免吗? 能用 suspend + withContext 替代吗?

什么时候该用它?

场景一: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,它们才是协程的日常。