相关文章:kotlin协程:一文搞懂各种概念
前言
本文全面解释协程的异常传递机制以及处理方式,需要一定的协程基础。摆脱只会使用 try catch 的尴尬,以更优雅和更灵活的方式处理异常。
异常传递
Job
对于普通 Job
来说,异常的传递是双向的,即异常会向子协程和父协程传播,流程为:
- 当前协程出现异常
cancel
子协程- 等待子协程
cancel
完成,cancel
自己 - 传递给父协程并循环前面步骤
可见,异常会自下而上地传播,任一协程发生异常会影响到整个协程树。
如下代码中,child 1出现异常后,child 3、child 2、child 0 依次会被取消。
kotlin
CoroutineScope(Job()).launch { // child 0
launch {// child 1
launch { // child 3
delay(1000)
println("child 3")
}
throw Exception("test")
}
launch { // child 2
delay(1000)
println("child 2")
}
delay(1000)
println("child 0")
}.join()
SupervisorJob
如果不希望协程内的异常向上传播或影响同级协程。可以使用 SupervisorJob
。
SuperVisorJob
可以使子协程的异常向上传播到 SupervisorJob
层时不被处理,即异常向上传播在 SupervisorJob
处终止。
如上图所示,SupervisorJob
或 Scope
的任一直接子协程 发生异常都不会影响其他直接子协程,更不会向上传播影响父协程。
注意,父协程异常依然会导致子协程取消,这点和
Job
一致。
kotlin
CoroutineScope(Job()).launch { // child 0
val supervisorJob = SupervisorJob()
launch(supervisorJob) {// child 1
launch { // child 3
delay(1000)
println("child 3")
}
throw Exception("test")
}
launch(supervisorJob) { // child 2
delay(1000)
println("child 2")
}
delay(1000)
println("child 0")
}.join()
还是上面的例子,使用 SupervisorJob
作为 child 1 和 child 2 的父 Job,这样 child 2 中的异常只能向下影响 child 3 ,避免了异常影响到 child 0 和 child 2。
也可以使用 supervisorScope
,它是一个自带了 SupervisorJob
的协程作用域,效果是一样的:
kotlin
CoroutineScope(Job()).launch { // child 0
supervisorScope {
launch {// child 1
launch { // child 3
delay(1000)
println("child 3")
}
throw Exception("test")
}
launch { // child 2
delay(1000)
println("child 2")
}
}
delay(1000)
println("child 0")
}.join()
不同的是,supervisorScope
是挂起方法,会挂起协程直至所有子协程执行完成。
注意,易错 💥
下面代码中,谁是 Child1 的父协程?
kotlin
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
launch {
// Child 1
}
launch {
// Child 2
}
}
Child 的父协程类型是 Job
,并不是 SupervisorJob
!
scope.launch
会创建一个新 Job
,该 Job
的父 Job 是 launch 时指定的,所以 SupervisorJob
是新 Job
的父 Job ,而 scope 创建时传入的 Job 被 SupervisorJob
覆盖,因而协程关系为:
SupervisorJob -〉Job -〉(child1、child2),因而上述代码中,SupervisorJob
没有起到该有的作用。
鉴于上述原因,可以:
kotlin
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// Child 1
}
scope.launch {
// Child 2
}
也可以使用 supervisorScope
达到预期效果:
scss
supervisorScope {
launch { // Child 1
throw IllegalArgumentException()
}
launch { // Child 2
delay(1000)
println("child 2 run over") //可顺利执行完成
}
delay(1000)
println("Job: ${coroutineContext[Job]?.javaClass}")
}
打印结果:
arduino
Exception in thread "Test worker @coroutine#2" java.lang.IllegalArgumentException
Job: class kotlinx.coroutines.SupervisorCoroutine
child 2 run over
不管是什么 Job,异常传递如果不做处理,最终会到达线程的 UncaughtExceptionHandler ,如果是 JVM 则输出在控制台,如果是 Android 没设置 UncaughtExceptionHandler 则会出现 app 崩溃(如果是主线程则一定崩溃)。
异常处理
对于不同协程构造器,异常的处理方式不同 。分别介绍 launch
和 async
情况下的异常处理。
Launch
-
try catch
launch 方式启动的协程,异常会在发生时立刻抛出,使用
try catch
就可以将协程中的异常捕获。如:kotlinscope.launch { try { codeThatCanThrowExceptions() } catch(e: Exception) { // Handle exception } }
try catch
整个协程也是可以的:kotlintry { coroutineScope { codeThatCanThrowExceptions() } } catch (t: Throwable) { // Handle exception }
注意,这样是不可以的 ❌ :
scsstry { CoroutineScope().launch { codeThatCanThrowExceptions() } } catch (t: Throwable) { // Handle exception }
因为 launch 不是挂起函数。
-
CoroutineExceptionHandler
除了使用
try catch
,更推荐使用CoroutineExceptionHandler
对异常进行统一处理。需要注意的是异常会层层代理到根协程,所以CoroutineExceptionHandler
只能在根协程中才能生效。比如这样 ✅ :
kotlinCoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex successfully") }) { throw RuntimeException() }
或者这样 ✅ ,根协程会继承该
CoroutineExceptionHandler
:kotlinCoroutineScope(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex successfully") }).launch() { throw RuntimeException() }
而这样是不对的 ❌ :
kotlincoroutineScope { launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex failed") }) { throw RuntimeException() } }
子协程异常会代理给父协程,一直向上传递直到根协程,如果找到
CoroutineExceptionHandler
则处理,否则走UncaughtExceptionHandler
。可见,其他子协程中的CoroutineExceptionHandler
不会起到作用。猜猜被谁捕获?:
kotlinCoroutineScope(Job() + CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex in scope") }).launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex in top Coroutine") }) { throw RuntimeException() }
结论:scope 中的
CoroutineExceptionHandler
会覆盖。这种将异常代理给父协程的行为可以被
SupervisorJob
改变,将异常交给子协程自己处理:kotlinsupervisorScope { launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex successfully") }) { throw RuntimeException() } }
异常传递到
SupervisorJob
处停止,交由SupervisorJob
的直接子协程处理,这时CoroutineExceptionHandler
是生效的。根协程:由
scope
直接调用launch
或async
开启的协程。
Async
-
try catch
当
async
开启的协程为根协程,或SupervisorJob
的直接子协程时,异常在调用await
时抛出,使用try catch
可以捕获异常:kotlin/** * async 开启的协程为根协程 */ fun main() = runBlocking { val deferred = GlobalScope.async { throw Exception() } try { deferred.await() //抛出异常 } catch (t: Throwable) { println("捕获异常:$t") } }
kotlin/** * async 开启的协程为 SupervisorJob 的直接子协程 */ fun main() = runBlocking { supervisorScope { val deferred = async { throw Exception() } try { deferred.await() //抛出异常 } catch (t: Throwable) { println("捕获异常:$t") } } }
kotlin/** * async 开启的协程为 SupervisorJob 的直接子协程 */ CoroutineScope(Job()).launch { val deferred = async(SupervisorJob()) { throw Exception() } try { deferred.await() //抛出异常 } catch (t: Throwable) { println("捕获异常:$t") } }
-
CoroutineExceptionHandler
当:
"
async
开启的协程为根协程 或supervisorScope
的直接子协程"的条件不成立时,异常会在发生时立刻抛出并传播,对
await
进行try catch
就不起作用了:kotlinfun main(): Unit = runBlocking { supervisorScope { launch { val deferred = async { throw Exception() } //抛出异常 try { deferred.await() } catch (e: Exception) { println("catch ex") } delay(1000) println("done") } } }
控制台中虽然打印了 "catch ex",但未能打印 "done",这表明异常传递到了父协程,父协程被取消。至于为什么还能打印 "catch ex",是因为 deferred.await() 会抛出异常,async 中的异常总会在 await 时抛出。如果在 await 前加个 delay,那么就看不到 "catch ex" 了,因为 await 被取消了。
可以使用
CoroutineExceptionHandler
进行处理。CoroutineExceptionHandler
只在根协程和SupervisorJob
的直接子协程有效。因此需要在launch
开启的父协程进行处理:kotlin/** * async 开启的协程非 supervisorScope 的直接子协程,异常会直接抛出, * try catch await 无效,但可由 CoroutineExceptionHandler 处理 */ fun main(): Unit = runBlocking { supervisorScope { launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("CoroutineExceptionHandler 捕获异常:$throwable") }) { val deferred = async { throw Exception() } //抛出异常 deferred.await() } } }
kotlin/** * async 开启的协程非根协程,异常会直接抛出, * try catch await 无效,但可由 CoroutineExceptionHandler 处理 */ fun main(): Unit = runBlocking { CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("CoroutineExceptionHandler 捕获异常:$throwable") }) { val deferred = async { throw Exception() } //抛出异常 deferred.await() }.join() }
总的来说,不管是 launch 还是 async,使用 CoroutineExceptionHandler
的规则都是一致的,也更不易出错,推荐使用。
Cancellation 和 异常
CancellationException 总会被 CoroutineExceptionHandler 忽略,但能被 try catch 捕获,ok,又多了个使用 CoroutineExceptionHandler
的理由。
kotlin
fun main() = runBlocking {
CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
println("CoroutineExceptionHandler 捕获异常:$throwable")
}) {
println("运行了")
cancel()
try {
delay(1000)
} catch (throwable: Throwable) {
println("try catch 捕获异常:$throwable")
throw throwable
}
}.join()
}
运行结果:
arduino
运行了
try catch 捕获异常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@54cda7ca
异常聚合
如果一个协程抛出不止一个异常,那么则以第一个抛出的异常为主,其他异常作为 suppressed
异常依附在主异常中。
源码
关于异常的传播控制逻辑可参见 JobSupport.kt
类的 notifyCancelling
和 childCancelled
方法,SupervisorJob
就是复写了 childCancelled
方法才阻止了异常的向上传播。
结构化并发
它是一种编程范式,旨在通过结构化的方式使并发编程 更清晰明确、更高质量、更易维护。
其核心有几点:
- 通过把多线程任务进行结构化的包装,使其具有明确的开始和结束点,并确保其孵化出的所有任务在退出前全部完成。
- 这种包装允许结构中线程发生的异常能够传播至结构顶端的作用域,并且能够被该语言原生异常机制捕获。
kotlin 协程设计中的协程关系、执行顺序、异常传播/处理 都符合结构化并发。结构化并发明确了并发任务什么时候开始,什么时候结束,异常如何传播,通过控制顶层结构具柄就可实现整个并发结构的取消、异常处理,使复杂的并发问题简单、清晰、可控。可以说,结构化并发大大降低了并发编程的难度。
总结
- 协程中未捕获的异常总会向下取消子协程,向上传递异常,体现了结构化并发的特点。
SupervisorJob
可以阻止异常继续向上传播,并将异常交给子协程处理。launch
启动的协程在异常发生时总是立刻抛出,可以由try catch
捕获,也可以使用CoroutineExceptionHandler
处理,注意 handler 使用的位置。- 而
async
启动的协程在其为根协程或supervisorScope
的直接子协程时,异常会在async
内部捕获,当deferred
对象调用await
时抛出,否则也会在异常发生时立刻抛出并传播。前者可以使用try catch
捕获await
调用,当然CoroutineExceptionHandler
也可以,而后者不能通过try catch
整个async
代码块或await
调用捕获异常。 - 结构化并发大大降低了并发编程的难度,kotlin 协程设计也遵循该编程范式。