Kotlin 协程异常处理机制
一直以为协程的异常机制很复杂,网上讲的术语一大堆,什么传播机制、结构化并发、协程作用域......看得我脑壳疼。但最近我研究了一下,也自己写代码测了一下,发现其实没有那么难。
简单入门,如有错误,欢迎指正
下面介绍几个核心的概念
1. 一个协程崩了,默认会把整个协程组都崩掉
为什么要强调这个呢,在Android里面线程不也是这样的吗?我们在开发Android的时候,一个线程崩了,整个应用就挂了。比如下面的代码:
kotlin
// Android Studio中运行,应用启动就崩了
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
thread {
throw new RuntimeException("子线程崩了 ❌");
}
}
}
一启动就崩了,所以我一直以为子线程崩溃了整个应用就会崩,直到我在IDEA中运行了同样的代码,如下:
kotlin
fun main() {
thread {
Thread.sleep(100)
throw RuntimeException("线程 A 崩了 ❌")
}
thread {
Thread.sleep(1000)
println("线程 B 还活着 ✅")
}
Thread.sleep(1000)
println("主线程也活着 ✅")
}
会发现一个线程崩了之后,主线程和其他的子线程竟然都还活着,于是我查了一下相关的资料,终于搞明白了。
原来是因为:在Java中线程未捕获的异常会由UncaughtExceptionHandler
处理,而Java默认是没有实现的,而Android实现了这个UncaughtExceptionHandler
。
看看Android是怎么实现的(在RuntimeInit
中的KillApplicationHandler
)
java
public void uncaughtException(Thread t, Throwable e) {
try {
...
} catch (Throwable t2) {
...
} finally {
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid());
System.exit(10);
}
}
Android在finally中执行了Process.killProcess(Process.myPid())
和System.exit(10)
,杀掉了线程,所以Android中线程异常应用是会崩溃的。
那么协程呢,运行下面这段代码:
kotlin
import kotlinx.coroutines.*
fun main() {
runBlocking {
coroutineScope {
launch {
delay(100)
throw RuntimeException("协程 A 崩了 ❌")
}
launch {
delay(1000)
println("协程 B 应该活着 ❌,但没机会执行")
}
}
}
}
运行之后发现,一个协程崩了,其他的协程也没有继续执行了。
原来在Kotlin中 由此我们可以总结下:
- 在线程中一个线程崩,不会影响其他线程。
- 在Android中由于系统的处理,无论哪个线程崩,整个应用都会退出。(除非自己调用了
setDefaultUncaughtExceptionHandler
) - 而协程和线程是不一样的。如果在同一个
coroutineScope
里跑多个协程,只要一个出错,其他协程也会被一起取消。
2. 如果不想都崩掉,就用 supervisorScope
Kotlin 提供了一个 supervisorScope
(或者 SupervisorJob()
),它的意思是:
"我只管我自己,我崩了不管别人,别人崩了也别来拉我下水。"
kotlin
fun main() {
runBlocking {
supervisorScope {
launch {
delay(100)
throw RuntimeException("协程 A 崩了 ❌")
}
launch {
delay(1000)
println("协程 B 仍然正常 ✅")
}
}
}
}
或者用SupervisorJob
kotlin
fun main() {
runBlocking {
val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisor)
scope.launch {
delay(100)
throw RuntimeException("协程 A 崩了 ❌")
}
scope.launch {
delay(1000)
println("协程 B 仍然正常 ✅")
}
delay(2000) // 等待所有子协程完成
}
}
这段代码就能明显看出来:一个崩了,另一个还能活得好好的。
3. 最后的兜底:CoroutineExceptionHandler
如果忘了 try-catch,协程崩了可以用这个兜底。它会在 launch
抛异常时触发,但对 async
是无效的。
kotlin
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获到异常:${exception.message} ✅")
}
fun main() = runBlocking {
val job = launch(handler) {
throw RuntimeException("我崩了 ❌")
}
job.join()
}
但下面这种 async 是不会被 handler 处理的:
kotlin
val handler = CoroutineExceptionHandler { _, exception ->
println("Handler 触发了:${exception.message}")
}
fun main() {
runBlocking {
val scope = CoroutineScope(handler)
scope.launch {
throw RuntimeException("协程 A 崩了 ❌")
}
val deferred = scope.async {
throw RuntimeException("async 崩了 ❌")
}
scope.launch {
delay(1000)
println("协程 B 应该活着 ❌,但没机会执行")
}
deferred.await()
// 必须用 try-catch,否则照样崩
// try {
// deferred.await()
// } catch (e: Exception) {
// println("自己捕获 async 异常 ✅")
// }
Unit
}
}
5. viewModelScope
和 lifecycleScope
viewModelScope
内部使用了SupervisorJob
,所以其中的子协程是互不影响的,崩一个不会带崩其他。
kotlin
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
lifecycleScope
,也是 supervisor。
kotlin
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
总结一下
- 一个线程崩了,默认不会影响其他线程,但是Android做了特殊处理,会将应用杀掉
- 一个协程崩了,默认会 cancel同scope里的全部协程(不像线程那么"独立");
- 用
supervisorScope
(或SupervisorJob
)可以避免"崩一个全挂"; CoroutineExceptionHandler
可以兜住launch
异常,但兜不了 async,async 要自己 try-catch。- Android 中
viewModelScope
和lifecycleScope
都用了SupervisorJob
。