Kotlin:非挂起函数中不使用runBlocking等待协程的方案?
首先针对你给出的代码替换需求:其实没有办法完全不用runBlocking又不把doSomething()改成挂起函数,因为普通非挂起函数必须同步返回结果,而await()是挂起函数,只能在协程作用域内调用。不过你可以优化runBlocking的上下文参数,让协程在更合适的调度器上执行,避免阻塞关键线程。修改后的代码如下:
fun doSomething(): Int { val job1 = GlobalScope.async(Dispatchers.Default) { calculateSomething() } val job2 = GlobalScope.async(Dispatchers.Default) { calculateSomething() } // 根据任务类型指定调度器:CPU密集型用Default,IO密集型用IO return runBlocking(Dispatchers.Default) { job1.await() + job2.await() } } suspend fun calculateSomething(): Int { delay(1000L) return 13 }
这里给async也指定了调度器,避免默认继承当前线程的上下文(比如如果doSomething在主线程调用,async默认会在主线程执行,可能阻塞UI)。
接下来逐个解答你的核心问题:
1. 阻塞代码与协程之间的正确桥接方式是什么?
官方推荐的标准方式就是**runBlocking**,它的设计初衷就是作为阻塞代码和挂起函数之间的桥梁:
- 它会阻塞当前调用线程,直到内部的协程执行完毕
- 可以通过参数指定协程运行的上下文(比如
Dispatchers.IO/Dispatchers.Default),避免阻塞关键线程(比如主线程) - 它会创建一个协程作用域,自动管理内部协程的生命周期,避免内存泄漏
注意:不要用Thread.sleep()或者手动创建线程来等待协程结果,这会失去协程的优势,runBlocking是最简洁且符合协程设计理念的桥接方式。
2. 非挂起函数中等待挂起函数的正确方法是什么?
答案还是**runBlocking**。因为挂起函数的调用必须在协程作用域或者另一个挂起函数内,非挂起函数没有天然的协程作用域,所以需要通过runBlocking创建一个临时的协程作用域,阻塞当前线程直到挂起函数执行完成并返回结果。
要避免一个误区:不要直接调用GlobalScope.async { ... }.await()然后用线程阻塞等待,因为await()本身是挂起函数,还是需要在协程作用域内调用,而且GlobalScope的协程生命周期不受控,可能导致资源泄漏。
3. 是否必须让main函数用runBlocking,全程处于协程作用域才能使用协程?
不是必须的,但分场景:
- 对于普通的Kotlin/JVM应用,
main函数作为程序入口,如果要调用挂起函数或者启动协程并等待其完成,确实需要runBlocking——如果直接用GlobalScope.launch,main函数会直接退出,后台协程可能还没执行完。 - 对于Android、Spring等框架应用,框架已经提供了内置的协程作用域(比如
viewModelScope、CoroutineScope(Dispatchers.IO)),你不需要在main函数中用runBlocking,直接在框架提供的作用域内使用协程即可。 - 如果你只是想启动后台协程不等待结果,可以用
GlobalScope.launch,但不推荐,因为GlobalScope的协程没有绑定到任何生命周期,容易造成内存泄漏,最好自己创建一个自定义的CoroutineScope来管理协程生命周期。
最后说你的RunningCommand工具类疑问:
在工具类中重复调用runBlocking是完全可行的,尤其是当工具类需要同步返回结果时(比如等待命令执行完成)。不过要注意两点:
- 给
runBlocking指定合适的调度器,比如处理IO操作的命令用Dispatchers.IO,避免阻塞调用线程 - 如果工具类会被高频调用,协程的调度开销比线程小很多,所以比原来的线程实现更高效,无需过度担忧。
内容的提问来源于stack exchange,提问作者Amir Abiri

