Kotlin Coroutines(協(xié)程) 完全解析系列:
Kotlin Coroutines(協(xié)程) 完全解析(一)崎溃,協(xié)程簡介
Kotlin Coroutines(協(xié)程) 完全解析(二)约啊,深入理解協(xié)程的掛起、恢復與調(diào)度
Kotlin Coroutines(協(xié)程) 完全解析(三)穆端,封裝異步回調(diào)、協(xié)程間關(guān)系及協(xié)程的取消
Kotlin Coroutines(協(xié)程) 完全解析(四)仿便,協(xié)程的異常處理
Kotlin Coroutines(協(xié)程) 完全解析(五)体啰,協(xié)程的并發(fā)
本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1
在上一篇文章中提到子協(xié)程拋出未捕獲的異常時默認會取消其父協(xié)程嗽仪,而拋出CancellationException
卻會當作正常的協(xié)程結(jié)束不會取消其父協(xié)程荒勇。本文來詳細解析協(xié)程中的異常處理,拋出未捕獲異常后協(xié)程結(jié)束后運行會不會崩潰钦幔,可以攔截協(xié)程的未捕獲異常嗎,如何讓子協(xié)程的異常不影響父協(xié)程常柄。
Kotlin 官網(wǎng)文檔中有關(guān)于協(xié)程異常處理的文章鲤氢,里面的內(nèi)容本文就不再重復俗壹,所以讀者們先閱讀官方文檔:
看完官方文檔后档叔,可能還是會有一些疑問:
launch
式協(xié)程的未捕獲異常為什么會自動傳播到父協(xié)程,為什么對異常只是在控制臺打印而已宁昭?async
式協(xié)程的未捕獲異常為什么需要依賴用戶來最終消耗異常喷市?自定義的
CoroutineExceptionHandler
的是如何生效的相种?異常的聚合是怎么處理的?
SupervisorJob
和supervisorScope
實現(xiàn)異常單向傳播的原理是什么品姓?
這些疑問在本文逐步解析協(xié)程中異常處理的流程時寝并,會一一解答箫措。
1. 協(xié)程中異常處理的流程
從拋出異常的地方開始跟蹤協(xié)程中異常處理的流程,拋出異常時一般都在協(xié)程的運算邏輯中衬潦。而在第二篇深入理解協(xié)程的掛起斤蔓、恢復與調(diào)度中提到在協(xié)程的三層包裝中,運算邏輯在第二層BaseContinuationImpl
的resumeWith()
函數(shù)中的invokeSuspend
運行镀岛,所以再來看一次:
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
...
var param = result
while (true) {
with(current) {
val completion = completion!!
val outcome: Result<Any?> =
try {
// 調(diào)用 invokeSuspend 方法執(zhí)行弦牡,執(zhí)行協(xié)程的真正運算邏輯
val outcome = invokeSuspend(param)
// 協(xié)程掛起時 invokeSuspend 才會返回 COROUTINE_SUSPENDED,所以協(xié)程掛起時漂羊,其實只是協(xié)程的 resumeWith 運行邏輯執(zhí)行完成驾锰,再次調(diào)用 resumeWith 時,協(xié)程掛起點之后的邏輯才能繼續(xù)執(zhí)行
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
// 注意這個 catch 語句走越,其實協(xié)程運算中所有異常都會在這里被捕獲椭豫,然后作為一種運算結(jié)果
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// 這里實際調(diào)用的是其父類 AbstractCoroutine 的 resumeWith 方法,當捕獲到異常時买喧,調(diào)用 resumeWith(Result.failure(exception)) 更新協(xié)程狀態(tài)
completion.resumeWith(outcome)
return
}
}
}
}
}
從上面源碼的try {} catch {}
語句來看捻悯,首先協(xié)程運算過程中所有未捕獲異常其實都會在第二層包裝中被捕獲,然后會通過AbstractCoroutine.resumeWith(Result.failure(exception))
進入到第三層包裝中淤毛,所以協(xié)程的第三層包裝不僅維護協(xié)程的狀態(tài)今缚,還處理協(xié)程運算中的未捕獲異常。這在第三篇分析子協(xié)程拋出未捕獲異常低淡,默認情況會取消其父線程時也提到過姓言。
繼續(xù)跟蹤 AbstractCoroutine.resumeWith(Result.failure(exception)) -> JobSupport.makeCompletingOnce(CompletedExceptionally(exception), defaultResumeMode) -> JobSupport.tryMakeCompleting(state, CompletedExceptionally(exception), defaultResumeMode),在最后tryMakeCompleting()
過程中部分關(guān)鍵代碼:
private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?, mode: Int): Int {
...
// process cancelling notification here -- it cancels all the children _before_ we start to to wait them (sic!!!)
// 該情景下蔗蹋,notifyRootCause 的值為 exception
notifyRootCause?.let { notifyCancelling(list, it) }
// now wait for children
val child = firstChild(state)
if (child != null && tryWaitForChild(finishing, child, proposedUpdate))
return COMPLETING_WAITING_CHILDREN
// otherwise -- we have not children left (all were already cancelled?)
// 已取消所有子協(xié)程后何荚,更新該協(xié)程的最終狀態(tài)
if (tryFinalizeFinishingState(finishing, proposedUpdate, mode))
return COMPLETING_COMPLETED
// otherwise retry
return COMPLETING_RETRY
}
先看notifyCancelling(state.list, exception)
函數(shù):
private fun notifyCancelling(list: NodeList, cause: Throwable) {
// first cancel our own children
onCancellation(cause)
// 這里會調(diào)用 handle 節(jié)點的 invoke() 方法取消子協(xié)程,具體點就是調(diào)用 childJob.parentCancelled(job) 取消子協(xié)程
notifyHandlers<JobCancellingNode<*>>(list, cause)
// then cancel parent
// 然后可能會取消父協(xié)程
cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
}
private fun cancelParent(cause: Throwable): Boolean {
// CancellationException is considered "normal" and parent is not cancelled when child produces it.
// This allow parent to cancel its children (normally) without being cancelled itself, unless
// child crashes and produce some other exception during its completion.
// CancellationException 是正常的協(xié)程結(jié)束行為猪杭,手動拋出 CancellationException 也不會取消父協(xié)程
if (cause is CancellationException) return true
// cancelsParent 屬性也可以決定出現(xiàn)異常時是否取消父協(xié)程餐塘,不過一般該屬性都為 true
if (!cancelsParent) return false
// parentHandle?.childCancelled(cause) 最后會通過調(diào)用 parentJob.childCancelled(cause) 取消父協(xié)程
return parentHandle?.childCancelled(cause) == true
}
所以出現(xiàn)未捕獲異常時,首先會取消所有子協(xié)程皂吮,然后可能會取消父協(xié)程戒傻。而有些情況下并不會取消父協(xié)程,一是當異常屬于 CancellationException 時蜂筹,而是使用SupervisorJob
和supervisorScope
時需纳,子協(xié)程出現(xiàn)未捕獲異常時也不會影響父協(xié)程,它們的原理是重寫 childCancelled() 為override fun childCancelled(cause: Throwable): Boolean = false
艺挪。
launch
式協(xié)程和async
式協(xié)程都會自動向上傳播異常不翩,取消父協(xié)程。
接下來再看tryFinalizeFinishingState
的實現(xiàn):
private fun tryFinalizeFinishingState(state: Finishing, proposedUpdate: Any?, mode: Int): Boolean {
...
// proposedException 即前面未捕獲的異常
val proposedException = (proposedUpdate as? CompletedExceptionally)?.cause
// Create the final exception and seal the state so that no more exceptions can be added
var suppressed = false
val finalException = synchronized(state) {
val exceptions = state.sealLocked(proposedException)
val finalCause = getFinalRootCause(state, exceptions)
// Report suppressed exceptions if initial cause doesn't match final cause (due to JCE unwrapping)
// 如果在處理異常過程還有其他異常,這里通過 finalCause.addSuppressedThrowable(exception) 的方式記錄下來
if (finalCause != null) suppressed = suppressExceptions(finalCause, exceptions) || finalCause !== state.rootCause
finalCause
}
...
// Now handle exception if parent can't handle it
// 如果 finalException 不是 CancellationException口蝠,而且有父協(xié)程且不為 SupervisorJob 和 supervisorScope器钟,cancelParent(finalException) 都返回 true
// 也就是說一般情況下出現(xiàn)未捕獲的異常,一般會傳遞到最根部的協(xié)程亚皂,由最頂端的協(xié)程去處理
if (finalException != null && !cancelParent(finalException)) {
handleJobException(finalException)
}
...
// And process all post-completion actions
completeStateFinalization(state, finalState, mode, suppressed)
return true
}
上面代碼中if (finalException != null && !cancelParent(finalException))
語句可以看出俱箱,除非是 SupervisorJob 和 supervisorScope,一般協(xié)程出現(xiàn)未捕獲異常時灭必,不僅會取消父協(xié)程狞谱,一步步取消到最根部的協(xié)程,而且最后還由最根部的協(xié)程(Root Coroutine)處理協(xié)程禁漓。下面繼續(xù)看處理異常的handleJobException
的實現(xiàn):
// JobSupport
protected open fun handleJobException(exception: Throwable) {}
// Builders.common.kt
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active) {
override val cancelsParent: Boolean get() = true
override fun handleJobException(exception: Throwable) = handleExceptionViaHandler(parentContext, exception)
}
// Actor
private open class ActorCoroutine<E>(
...
) : ChannelCoroutine<E>(parentContext, channel, active), ActorScope<E> {
override fun onCancellation(cause: Throwable?) {
_channel.cancel(cause)
}
override val cancelsParent: Boolean get() = true
override fun handleJobException(exception: Throwable) = handleExceptionViaHandler(parentContext, exception)
}
默認的handleJobException
的實現(xiàn)為空跟衅,所以如果 Root Coroutine 為async
式協(xié)程,不會有任何異常打印操作播歼,也不會 crash伶跷,但是為launch
式協(xié)程或者actor
式協(xié)程的話,會調(diào)用handleExceptionViaHandler()
處理異常秘狞。
下面接著看handleExceptionViaHandler()
的實現(xiàn):
internal fun handleExceptionViaHandler(context: CoroutineContext, exception: Throwable) {
// Invoke exception handler from the context if present
try {
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
// 如果協(xié)程有自定義 CoroutineExceptionHandler叭莫,則只調(diào)用 handler.handleException() 就返回
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
// If handler is not present in the context or exception was thrown, fallback to the global handler
// 如果沒有自定義 CoroutineExceptionHandler,
handleCoroutineExceptionImpl(context, exception)
}
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
// use additional extension handlers
// 在 Android 中烁试,還會有 uncaughtExceptionPreHandler 作為額外的 handlers
for (handler in handlers) {
try {
handler.handleException(context, exception)
} catch (t: Throwable) {
// Use thread's handler if custom handler failed to handle exception
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
}
}
// use thread's handler
val currentThread = Thread.currentThread()
// 調(diào)用當前線程的 uncaughtExceptionHandler 處理異常
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
// Thread.java
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
// 當前線程沒有定義 uncaughtExceptionHandler雇初,會返回線程組作為 Thread.UncaughtExceptionHandler
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
// ThreadGroup.java
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
// 優(yōu)先使用線程通用的 DefaultUncaughtExceptionHandler,如果也沒有的話减响,則在控制臺打印異常堆棧信息
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
所以默認情況下靖诗,launch
式協(xié)程對未捕獲的異常只是打印異常堆棧信息,如果在 Android 中還會調(diào)用uncaughtExceptionPreHandler
處理異常支示。但是如果使用了 CoroutineExceptionHandler 的話刊橘,只會使用自定義的 CoroutineExceptionHandler 處理異常。
到這里協(xié)程的異常處理流程就走完了颂鸿,但是還有一個問題還沒解答促绵,async
式協(xié)程的未捕獲異常只會導致取消自己和取消父協(xié)程,又是如何依賴用戶來最終消耗異常呢嘴纺?
fun main(args: Array<String>) = runBlocking<Unit> {
val deferred = GlobalScope.async {
println("Throwing exception from async")
throw IndexOutOfBoundsException()
}
// await() 恢復調(diào)用者協(xié)程時會重寫拋出異常
deferred.await()
}
看看反編譯的 class 文件就明白了:
public final Object invokeSuspend(@NotNull Object result) {
Object coroutine_suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Deferred deferred;
switch (this.label) {
case 0:
if (result instanceof Failure) {
throw ((Failure) result).exception;
}
CoroutineScope coroutineScope = this.p$;
// 創(chuàng)建并啟動一個新的 async 協(xié)程
deferred = BuildersKt.async$default((CoroutineScope) GlobalScope.INSTANCE, null, null, (Function2) new 1(null), 3, null);
this.L$0 = deferred;
this.label = 1;
// await() 掛起函數(shù)掛起當前協(xié)程败晴,等待 async 協(xié)程的結(jié)果
if (deferred.await(this) == coroutine_suspended) {
return coroutine_suspended;
}
break;
case 1:
deferred = (Deferred) this.L$0;
// async 協(xié)程恢復當前協(xié)程時,傳遞進來的結(jié)果是 CompletedExceptionally(IndexOutOfBoundsException())
if (result instanceof Failure) {
// 在當前協(xié)程重新拋出 IndexOutOfBoundsException 異常
throw ((Failure) result).exception;
}
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return Unit.INSTANCE;
}
所以async
式協(xié)程只有通過await()
將異常重新拋出颖医,不過可以可以通過try { deffered.await() } catch () { ... }
來捕獲異常位衩。
2. 小結(jié)
分析完協(xié)程的異常處理流程裆蒸,其中需要注意的問題有下面這些:
拋出 CancellationException 或者調(diào)用
cancel()
只會取消當前協(xié)程和子協(xié)程熔萧,不會取消父協(xié)程,也不會其他例如打印堆棧信息等的異常處理操作。拋出未捕獲的非 CancellationException 異常會取消子協(xié)程和自己佛致,也會取消父協(xié)程贮缕,一直取消 root 協(xié)程,異常也會由 root 協(xié)程處理俺榆。
如果使用了 SupervisorJob 或 supervisorScope感昼,子協(xié)程拋出未捕獲的非 CancellationException 異常不會取消父協(xié)程,異常也會由子協(xié)程自己處理罐脊。
launch
式協(xié)程和actor
式協(xié)程默認處理異常的方式只是打印堆棧信息定嗓,可以自定義 CoroutineExceptionHandler 來處理異常。async
式協(xié)程本身不會處理異常萍桌,自定義 CoroutineExceptionHandler 也無效宵溅,但是會在await()
恢復調(diào)用者協(xié)程時重新拋出異常。