Kotlin协程中runBlocking遇子协程异常时程序冻结且无日志的原因探究
嘿,这个问题我之前也碰到过,其实核心在于runBlocking和GlobalScope.launch的异常处理机制完全不同,咱们一步步拆解:
为什么runBlocking会导致冻结且无异常日志?
runBlocking是一个阻塞当前线程的协程构建器,核心作用是衔接协程世界和普通线程世界。当你在runBlocking内部调用awaitAll()时,async代码块里抛出的异常会被awaitAll()重新抛出,但它的传播路径很特殊:
- 异常从async协程传递到Deferred对象,再被
awaitAll()抛到runBlocking的协程上下文里; - runBlocking会直接把这个异常传播到调用它的线程(比如你的主线程);
- 如果调用线程没有用
try-catch捕获异常:- 控制台程序可能直接终止,你没来得及看到日志;
- Android这类GUI程序会触发ANR(应用无响应),主线程崩溃导致程序冻结,异常可能被系统机制吞掉或未及时输出。
这就是你看到“程序冻结、无日志”的原因——异常其实抛出来了,但直接搞崩了调用runBlocking的线程,导致程序无法继续执行,自然没法打印日志。
为什么换成GlobalScope.launch就正常了?
GlobalScope.launch是非阻塞的协程构建器,会把协程放到后台线程执行,而且它的默认上下文自带默认CoroutineExceptionHandler:
- 当
awaitAll()抛出异常时,会被launch的协程上下文捕获; - 默认异常处理器会把异常堆栈完整打印到日志(控制台或Logcat),同时不会影响主线程运行;
- 所以你能看到异常日志,程序也不会冻结——因为主线程根本没被阻塞,后台协程的异常被妥善处理并输出了。
正确的处理方式
不管用runBlocking还是launch,都应该显式处理异常,避免这类问题:
方案1:在runBlocking内加try-catch
fun doStuff() { runBlocking { try { (0 until configurableLimit) .map { index -> async(Dispatchers.IO) { // 可能抛异常的代码 } } .awaitAll() println("Complete") } catch (e: Exception) { // 处理并打印异常 e.printStackTrace() } } }
这样异常会被捕获,不会导致调用线程崩溃,你也能看到完整日志。
方案2:给协程指定CoroutineExceptionHandler
不管用runBlocking还是自定义CoroutineScope,都可以显式指定异常处理器:
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() } fun doStuff() { runBlocking(exceptionHandler) { (0 until configurableLimit) .map { index -> async(Dispatchers.IO) { // 可能抛异常的代码 } } .awaitAll() println("Complete") } }
即使异常没被try-catch捕获,也会被处理器打印出来。
另外提醒下:尽量不要用GlobalScope.launch——它是全局协程范围,生命周期难以管理,建议用自定义CoroutineScope(比如Android里的viewModelScope、lifecycleScope)。
内容的提问来源于stack exchange,提问作者craigmiller160
相关产品推荐
相关产品推荐

