在协程中使用runBlocking是否会引发问题?官方文档为何禁止?
这是个非常典型的协程入门坑,很多刚接触Kotlin协程的开发者都可能不小心踩进去,咱们一步步把这个问题说清楚:
为什么官方明确禁止在协程中调用runBlocking?
首先直接看官方文档的明确声明:
"This function should not be used from a coroutine"
要理解这句话,得先搞懂runBlocking的本质:它是专门用来打通阻塞式代码和协程代码的桥梁。它的设计初衷是在非协程环境(比如普通的main函数、传统的阻塞式线程任务)里启动协程,并且阻塞当前线程,直到内部的协程逻辑全部执行完毕。
而如果你已经在一个协程里面了,意味着你已经处于支持挂起的上下文环境中——协程的核心优势就是用非阻塞的挂起来替代线程阻塞,这时候再调用runBlocking,相当于放弃了协程的非阻塞特性,强行把当前线程卡住,完全违背了协程的设计理念,所以官方说这种用法“毫无意义”:你本来可以用launch、async直接启动子协程,或者直接调用挂起函数,根本没必要多此一举去阻塞线程。
嵌套runBlocking的简单场景表现
就像你写的这段测试代码:
fun main(args: Array<String>) { runBlocking { runBlocking { println("hi") } } }
它确实能编译运行,因为语法上没有错误,但IntelliJ会弹出警告——这是IDE在提醒你:你正在做不符合协程最佳实践的操作。
这段代码的执行逻辑其实是冗余的:
- 外层
runBlocking启动协程,同时阻塞main线程; - 内层
runBlocking又启动一个协程,再次阻塞当前的main线程(因为外层协程运行在main线程上); - 内层协程打印完成后,内层
runBlocking释放线程; - 外层协程完成,外层
runBlocking释放main线程。
说白了就是做了两次没必要的线程阻塞,纯粹是多余操作。
复杂场景下的致命风险:死锁与性能损耗
如果是在更复杂的业务场景里,比如使用了限定线程数量的Dispatcher(比如Dispatchers.IO、自定义的单线程Dispatcher),嵌套runBlocking很容易引发死锁。举个典型的例子:
fun main() = runBlocking(Dispatchers.IO) { println("外层协程运行在:${Thread.currentThread().name}") runBlocking(Dispatchers.IO) { println("内层协程运行在:${Thread.currentThread().name}") } }
这段代码大概率会触发死锁:
- 外层
runBlocking占用了Dispatchers.IO线程池中的一个线程; - 内层
runBlocking试图在Dispatchers.IO上启动协程,但如果此时IO线程池的所有线程都被占用(比如刚好达到最大并发数,或者你用的是单线程Dispatcher),它会一直等待线程可用; - 但外层的线程被内层
runBlocking死死阻塞着,永远不会释放——死锁就这样发生了,程序会卡在那里一动不动。
除此之外,这种用法还会带来性能损耗:线程是宝贵的系统资源,runBlocking会持续占用线程直到内部协程完成,过度阻塞会导致线程池耗尽,直接降低整个应用的并发处理能力。
总结
- 从设计初衷来看,
runBlocking是“从阻塞世界进入协程世界”的入口,在协程内部调用它完全违背设计逻辑,毫无意义; - 简单测试场景下可能不会崩溃,但会做冗余的线程阻塞;
- 复杂业务场景(尤其是使用限定Dispatcher时)极易引发死锁,还会拖垮应用性能;
- 永远遵循官方建议:不要在协程中调用
runBlocking,如果需要在协程内启动子任务,用launch、async或者直接调用挂起函数就好。
内容的提问来源于stack exchange,提问作者Tobias Hermann

