“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)JobAnd 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,catchCaptured. 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

  1. BaseContinuationImpltheresumeWithmethods
  2. StandaloneCoroutinethehandleJobExceptionmethods

2.1 BaseContinuationImplintroduce

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

  1. Our coroutine body implementation is implemented atinvokeSuspendIn the method
  2. When the coroutine body throws an exception, it is automaticallycatchLived in and packaged intoResult.failure
  3. Abnormal result passingcompletion.resumeWithContinue to throw up

2.2 StandaloneCoroutineintroduce

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:

  1. handleJobExceptionIt’s just a callhandleCoroutineExceptionmethods
  2. handleCoroutineExceptionThe method will first try fromcontextRemove theCoroutineExceptionHandler
  3. If we didn’t set itCoroutineExceptionHandler, is passed to the global exception handler, eventually using the threaduncaughtExceptionHandlerout

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:

  1. In the callhandleJobExcepionBefore, it will callcancelParent
  2. cancelParentIf yes is foundCancellationException, will return directlytrue, sohandleJobExcepionIt will not be executed and the exception will not be propagated upward

4. supervisorScopeException 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:

  1. usesupervisorScopeScope, which is passed in at startupSupervisorCoroutine
  2. SupervisorCoroutineRewrite thechildCancelledMethod, returnfalseAn exception that does not handle a subcoroutine, so the exception is truncated

5. coroutineScopeException 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

  1. An exception is thrown inside the coroutine
  2. Determine whetherCancellationExceptionIf yes, no processing is done
  3. Determines whether the parent coroutine is empty or issupervisorScopeIs called if it ishandleJobException, handle exceptions
  4. 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