“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”
preface
Before for coroutines exception mechanism made a methodological introduction, introduces the coroutines exception mechanism and elegant packaging, interested students can know: coroutines exception mechanism and elegant package | technical review
1. The coroutine catches exception 2 through the exception handler. CancellationException is thrown when a coroutine is cancelled, and 3 needs special treatment. There are different scopes in coroutines, and the abnormal propagation mechanism is different in different scopes
1. How does a coroutine exception handler work? 2. How to handle exceptions when coroutine is cancelled? 3. How is the exception propagation mechanism different in different scopes implemented? 4. Summary of coroutine anomaly propagation flow chart
1. Pre-knowledge
The main content of this article is to introduce the principle of kotlin coroutine exception propagation
1.1 Exception Handler
Kotlin exception handler that CoroutineExceptionHandler CoroutineExceptionHandler inheritance in CoroutineContext also added to the context when creating coroutines, When an exception occurs using the key from the context we usually use it like this:
val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
println("error")
}
viewModelScope.launch(handler) {
/ /...
}
Copy the code
1.2 Coroutine scope
Coroutine scopes are used to clarify the parent-child relationship between coroutines, and to propagate behavior such as cancellation or exception handling. They are divided into three categories:
- Top-level scope: The scope in which a coroutine has no parent coroutine is the top-level scope
- Cooperative scope: a coroutine starts a new coroutine that is a subcoroutine of the existing coroutine. In this case, the scope in which the subcoroutine is located defaults to the cooperative scope. Any uncaught exceptions thrown by the child coroutine are passed to the parent coroutine, and the parent coroutine is cancelled.
- Master-slave scope: consistent with the cooperative scope in the parent-child relationship of coroutines, the difference is that coroutines under this scope do not pass an uncaught exception up to the parent coroutine
1.3 Exception propagation mechanism
One of the most innovative features of coroutines is structured concurrency. To make all the functionality of structured concurrency possible, the Job objects of CoroutineScope and the Job objects of Coroutines and child-Coroutines form a hierarchy of parent-child relationships. Uncaught exceptions are first propagated upward until there is no parent coroutine to handle them. This exception propagation will result in the failure of the parent Job, which in turn will result in the cancellation of all jobs at the child level.
As shown above, exceptions to the subcoroutine propagate to the subcoroutine (1)Job
And then spread totopLevelScope
(2)Job
.
But if we use the supervisorScope in the middle, it will truncate the exception and spread up
2. How does the exception handler work?
We usually use CoroutineExceptionHandler to handle exceptions, simple example below
fun testExceptionHandler(a) {
val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
print("error")
}
viewModelScope.launch(handler) {
print("1")
throw NullPointerException()
print("2")}}Copy the code
So here’s the problem. Exceptions we usually usetry,catch
Captured. How does that translate into this?
This problem is actually quite simple, let’s make a break point to see the call stack:
So this is pretty straightforward, this is the call stack for exception passing inside the coroutine, and there are two main points here
BaseContinuationImpl
theresumeWith
methodsStandaloneCoroutine
thehandleJobException
methods
2.1 BaseContinuationImpl
introduce
We’ve seen this before when we talked about what coroutines really are. Our coroutine body is essentially a subclass of ContinuationImpl, which periodically calls back to the invokeSuspend method of BaseContinuationImpl if you’re not familiar with this: Coroutine bytecode decompile
Let’s look at the BaseContinuationImpl’s resumeWith method
public final override fun resumeWith(result: Result<Any? >) {
/ /...
valoutcome: Result<Any? > =try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
/ /...
completion.resumeWith(outcome)
return
}
Copy the code
The code here is also relatively simple
- Our coroutine body implementation is implemented at
invokeSuspend
In the method - When the coroutine body throws an exception, it is automatically
catch
Lived in and packaged intoResult.failure
- Abnormal result passing
completion.resumeWith
Continue to throw up
2.2 StandaloneCoroutine
introduce
From the initial call stack, we can see that the association is often passed to the StandaloneCoroutine. Where does this StandaloneCoroutine come from? This is actually the completion passed in when we start the coroutine, and also the completion in the completion.resumeWith(outcome) callback that the body of the coroutine calls back after completion. The starting code looks like this:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope. () - >Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
Copy the code
When we launch a coroutine, if we do not specify a launch mode, we will pass in the StandaloneCoroutine by default
Now let’s look at the handleJobException method
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active) {
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true}}public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
try {
// If an exception handler is set, it is pulled from the context and handledcontext[CoroutineExceptionHandler]? .let { it.handleException(context, exception)return}}catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
/ / if there is no set CoroutineExceptionHandler, were passed to the global exception handler
handleCoroutineExceptionImpl(context, exception)
}
Copy the code
It can be intuitively seen from the above:
handleJobException
It’s just a callhandleCoroutineException
methodshandleCoroutineException
The method will first try fromcontext
Remove theCoroutineExceptionHandler
- If we didn’t set it
CoroutineExceptionHandler
, is passed to the global exception handler, eventually using the threaduncaughtExceptionHandler
out
3. How to handle exceptions when coroutine cancellations?
We already know that cancelled coroutines will throw a CancellationException at the start of the suspension, and it will be ignored by the coroutine’s mechanism so why is CancellationException ignored?
Again, going back to the code and looking at the call stack above, JobSupport’s finalizeFinishingState method is called before calling handleJobException
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?).: Any? {
/ /...
if(finalException ! =null) {
val handled = cancelParent(finalException) || handleJobException(finalException)
if (handled) (finalState as CompletedExceptionally).makeHandled()
}
/ /...
}
private fun cancelParent(cause: Throwable): Boolean {
/ /...
CancellationException is considered normal
val isCancellation = cause is CancellationException
val parent = parentHandle
// If there is no parent coroutine, CancellationException is ignored, but other exceptions are still propagated
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
// Notifies the parent coroutine to check the subcoroutine cancellation status
return parent.childCancelled(cause) || isCancellation
}
Copy the code
As can be seen from the above:
- In the call
handleJobExcepion
Before, it will callcancelParent
cancelParent
If yes is foundCancellationException
, will return directlytrue
, sohandleJobExcepion
It will not be executed and the exception will not be propagated upward
4. supervisorScope
Exception propagation
As we mentioned above, supervisorScope will truncate the abnormal upward communication, did it? Let’s look at the code
public suspend fun <R> supervisorScope(block: suspend CoroutineScope. () - >R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
private class SupervisorCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
override fun childCancelled(cause: Throwable): Boolean = false
}
Copy the code
As can be seen from the above:
- use
supervisorScope
Scope, which is passed in at startupSupervisorCoroutine
SupervisorCoroutine
Rewrite thechildCancelled
Method, returnfalse
An exception that does not handle a subcoroutine, so the exception is truncated
5. coroutineScope
Exception propagation
We said earlier that coroutineScope propagates an uncaught exception first, and only handles it if there is no parent coroutine. Let’s look at an example
fun main(a) {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")}val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
launch(coroutineExceptionHandler) {
throw RuntimeException("RuntimeException in nested coroutine")
}
}
Thread.sleep(100)}/ / output
// Exception in thread "DefaultDispatcher-worker-2" java.lang.RuntimeException: RuntimeException in nested coroutine
Copy the code
Can you find the program or crash why does it not work? This is because to child coroutines set CoroutineExceptionHandler is of no effect, we must give top coroutines Settings, or initialize the Scope set is valid
Again, the principle is simple, so let’s review the code
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?).: Any? {
/ /...
if(finalException ! =null) {
val handled = cancelParent(finalException) || handleJobException(finalException)
if (handled) (finalState as CompletedExceptionally).makeHandled()
}
/ /...
}
Copy the code
When using CoroutineScope, as long as the parent coroutine is not empty, The handleJobException cancelParent returns true, always behind will not perform So give child coroutines set CoroutineExceptionHandler is of no effect, in order to make it work, You must set it to either a CoroutineScope or a top-level coroutine.
conclusion
This paper mainly analyzes the exception propagation mechanism of Kotlin coroutine, mainly divided into the following steps
- An exception is thrown inside the coroutine
- Determine whether
CancellationException
If yes, no processing is done - Determines whether the parent coroutine is empty or is
supervisorScope
Is called if it ishandleJobException
, handle exceptions - If not, the exception is passed to the parent coroutine, which then repeats the process
The above steps are summarized as the flowchart below:
The resources
Coroutines exception handling crack Kotlin coroutines (4) – exception handling Coroutines exception mechanism and elegant package | technical review