Hello everyone, my name is 🐜;

background

I have been using Kotlin’s coroutine for some time, and I am familiar with its common usage. However, it is still in the use stage, without in-depth understanding of the code, and still feels a little virtual. Taking advantage of the New Year this period of time, for the exception processing of coroutine, the relevant source code to learn a wave, comb and summarize their own understanding.

This article is based on Kotlin V1.4.0, Kotlin-Coroutines V1.3.9 source code analysis

1, CoroutineScope source code analysis

Role: Create and trace coroutines, manage parent-child relationships and structures between different coroutines

How to create coroutines:

1. Create via CoroutineScope

2. Create in coroutine

The first way, how do I create it through CoroutineScope in the first place?

val scope = CoroutineScope(Job() + Dispatchers.Main) @Suppress("FunctionName") public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] ! = null) context else context + Job()) internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context }Copy the code

CoroutineScope is a global method in which a CoroutineScope object is instantiated using ContextScope.

Similar to MainScope or viewModelScope and lifecycleScope on Android (except with some automatic cancel in the lifecycle related callback)

This is where I came from. In addition, a Job will be generated during scope initialization to play a role of tracking

The difference between GlobalScope and CoroutineScope is that GlobalScope’s Job is empty because its coroutineContext is EmptyCoroutineContext, which has no Job

With scope, we can create a coroutine through Launch

val job = scope.launch {}
Copy the code

Look at the poke code

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = Coroutinestart. DEFAULT, block: suspend CoroutineScope.() -> Unit): Job {... Omit code... return coroutine }Copy the code

The third argument is a lambda argument with a receiver (see Kotlin for receiver). The default type is CoroutineScope

Val job = scope. Launch {①/* this: Since launch is an extension method, the default receiver in the above example is this, so the following two methods are written the same. This is a callback. The handle is CoroutineScop launch {/*... */} this.launch {// Create a new coroutine as the parent of the current coroutine}}Copy the code

Look again at the implementation of coroutinescope.launch

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job {// create a new context based on the parent (the coroutine's parent context), and then create the coroutine as follows: Val coroutine = if (start.isLazy) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, block) Active = true) //start Coroutine. Start (start, coroutine, block) return coroutine } public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {// This is an extension of the CoroutineScope function. // Add a context to a context map data group. Val debug = if (debug) combined + val debug = if (debug) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined return if (combined ! == Dispatchers.Default && combined[ContinuationInterceptor] == null) debug + Dispatchers.Default else debug }Copy the code

How do the +’s add up? That’s where the related classes come in

CoroutineContext: Interface for all contexts

CombinedContext: Class generated when context is combined

Coroutinecontext. Element: Most single-context implementation classes, as some directly implement CoroutineContext

public operator fun plus(context: CoroutineContext): CoroutineContext = // Operator operator overload feature eg:Job() + dispatchers. IO + CoroutineName("test") will run here if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation Element is the addend context.fold(this) {acc, element -> val removed = acc.minusKey(element.key) if (removed === EmptyCoroutineContext) element else { // make sure interceptor is always last in the context (and thus is fast to get when present) val interceptor = removed[ContinuationInterceptor] if (interceptor == null) CombinedContext(removed, element) else { val left = removed.minusKey(ContinuationInterceptor) if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else CombinedContext(CombinedContext(left, element), interceptor) } } }Copy the code

A map (actually a single linked list, see how Kotlin CoroutineContext CoroutineContext can be summable) is used to retrieve different types of data by key. Create a new CoroutineContext using the current CoroutineContext.

Val scope = CoroutineScope(Job() + dispatchers.main)

Together with the above two code snippets, we can see that a new coroutine CoroutineContext consists of elements

1. There is an element, Job, that controls the life cycle of the coroutine

2. The remaining elements are inherited from the parent of the CoroutineContext, which may be another coroutine or the CoroutineScope that created it

2. CoroutineScope type

2.1 Influence of coroutine scope on anomaly propagation

Type:

Let me draw a picture for clarity:

Description:

When c2-1 is abnormal, C2-1->C2->C2-2->C2->C1->C3 (including its subcoroutines) ->C4

C3-1-1 abnormal occurrence,C3-1-1->C3-1-1-1, other not affected

C3-1-1-1 abnormal occurrence, C3-1-1-1->C3-1-1, other not affected

2.2. Schematic code

1. C1 and C2 are unrelated

Launch {// C1 globalscope. launch {// C2 //... }}Copy the code

C2 and C3 are subcoroutines of C1, C2 and C3 exceptions cancel C1

Globalscope. launch{// coroutine C1 coroutineScoope {launch{}// coroutine C2 launch{}// coroutine C3}}Copy the code

3, C2 and C3 are subcoroutines of C1, C2 and C3 exceptions do not cancel C1

GlobalScope. Launch {// c C1 supervisorScope {launch{} c c launch{}Copy the code

2.3. Take 🌰 for example

Eg1:

@Test
fun test()= runBlocking{
    val handler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("CoroutineExceptionHandler got $exception  coroutineContext ${coroutineContext}")
    }
    val job = GlobalScope.launch(handler) {
        println("1")
        delay(1000)
        coroutineScope {
            println("2")
            val job2 = launch(handler) {
                throwErrorTest()
            }
            println("3")
            job2.join()
            println("4")
        }
    }
    job.join()

}
fun throwErrorTest(){
    throw Exception("error test")
}
Copy the code

Output result:

If it is a coscope, an exception occurs in the coroutine where JoB2 is located, the job is cancelled (no “4” is printed), and the exception is thrown from the coroutine where the job is located

eg2:

@Test
fun test()= runBlocking{
    val handler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("CoroutineExceptionHandler got $exception  coroutineContext ${coroutineContext}")
    }
    val job = GlobalScope.launch(handler) {
        println("1")
        delay(1000)
        supervisorScope {
            println("2")
            val job2 = launch(handler) {
                throwErrorTest()
            }
            println("3")
            job2.join()
            println("4")
        }
    }
    job.join()

}
fun throwErrorTest(){
    throw Exception("error test")
}
Copy the code

Output result:

If job2 is in a master/slave scope, the job is not cancelled (a “4” is printed) and the exception is thrown by the joB2 coroutine

3. Source code analysis of exception processing process in coroutine

3.1. Three-layer packaging of coroutines

Layer 1: Jobs returned by launch and Async encapsulate the state of coroutines and provide interfaces to remove coroutines. Instances are all inherited from AbstractCoroutine

Layer 2: A compiler generated (CPS) subclass of SuspendLambda that encapsulates the real operational logic of the coroutine, inheriting from BaseContinuationImpl, where the Completion property is the first layer of the coroutine wrapper

Layer 3: DispatchedContinuation, which encapsulates thread scheduling logic and contains a second wrapper for coroutines

All three layers of wrappers implement the Continuation interface, combining layers of wrappers of coroutines in a proxy pattern, each responsible for a different function

The operational logic runs in invokeSuspend in the resumeWith() function of the second layer BaseContinuationImpl

3.2 The entry where the exception occurs

BaseContinuationImpl resumeWith(result: result <Any? >) handle exception logic, omitted part of the code

public final override fun resumeWith(result: Result) { val completion = completion!! // Fail fast when trying to resume continuation without completion val outcome: Result =... Other code... try { val outcome = invokeSuspend(param) if (outcome === COROUTINE_SUSPENDED) return Result.success(outcome) } catch (exception: Throwable) {result.failure (exception)}... Other code... Completion. ResumeWith (outcome)... Other code... }Copy the code

From the above code analysis

1. The specific implementation of invokeSuspend(PARAM) method is generated during compilation, corresponding to the processing logic of the coroutine body

2. When an exception occurs, that is, the outcome is result.failure (exception), which is specifically called in completion.

Through AbstractCoroutine. ResumeWith (Result. Failure (exception)) into the third layer in the packaging

Continue to follow AbstractCoroutine. ResumeWith (result: the result) – > JobSupport. MakeCompletingOnce (proposedUpdate: Any?) : Any? -> JobSupport.tryMakeCompleting(state: Any? , proposedUpdate: Any?) : Any? ->JobSupport.tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?) : Any?

In tryMakeCompletingSlowPath approach

var notifyRootCause: Throwable? = null synchronized(finishing) {//... Other code... notifyRootCause = finishing.rootCause.takeIf { ! wasCancelling } } // process cancelling notification here -- it cancels all the children _before_ we start to to wait them (sic!!!) // In this case, the value of notifyRootCause is Exception notifyRootCause? .let { notifyCancelling(list, it) } // otherwise -- we have not children left (all were already cancelled?) Return finalizeFinishingState(Finishing, proposedUpdate) // Other code...Copy the code

If an exception occurs, that is, notifyCancelling is not null, the notifyCancelling method is called to cancel the subcoroutine

private fun notifyCancelling(list: NodeList, cause: Throwable) {
    // first cancel our own children
    onCancelling(cause)
    notifyHandlers>(list, cause)
    // then cancel parent
    cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
}
Copy the code

Another method, finalizeFinishingState, is mainly the logic of exception passing and handling, the key code is as follows

private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?) : Any? {... Other code... // Now handle the final exception if (finalException ! CancelParent (finalException) {cancelParent(finalException); The current // coroutine handles handleJobException(finalException). Will be mentioned below) val handled = cancelParent (finalException) | | handleJobException (finalException) if (handled) (finalState as CompletedExceptionally). MakeHandled ()}. Other code... return finalState } /** * The method that is invoked when the job is cancelled to possibly propagate cancellation to the  parent. * Returns `true` if the parent is responsible for handling the exception, `false` otherwise. * * Invariant: never returns `false` for instances of [CancellationException], Otherwise to exception * may leak to the [CoroutineExceptionHandler]. * returns refers to is true, by the parent coroutines processing, */ private fun cancelParent(cause: Throwable): Boolean { // Is scoped coroutine -- don't propagate, will be rethrown /** * Returns `true` for scoped coroutines. * Scoped coroutine is a coroutine that is executed sequentially within the enclosing scope without any concurrency. * Scoped coroutines always handle any exception happened within -- they just rethrow it to the enclosing scope. * Examples of scoped coroutines are `coroutineScope`, 'withTimeout' and 'runBlocking'. */ / If isScopedCoroutine true, coroutineScope is the primary/secondary scope, If (isScopedCoroutine) return true // Cause is CancellationException CancellationException is considered "normal" and parent usually is not cancelled when child produces it. * CancellationException is considered "normal" and parent usually 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. */ val isCancellation = cause is CancellationException val parent = parentHandle // No parent -- ignore CE, report other exceptions. if (parent === null || parent === NonDisposableHandle) { return isCancellation } // Notify Parent but don't forget to check cancellation //childCancelled(cause) is false, While the child is handling the container with the handling job and container scope, it is also safe for the container to handle uncaught exceptions. ChildCancelled () : override fun childCancelled(cause: Throwable): Boolean = false return parent.childCancelled(cause) || isCancellation }Copy the code

From the above code

1. When an uncaught exception occurs, all subcoroutines are cancelled first

2. If the exception is a CancellationException, the parent coroutine will not be cancelled

The container is designed for handling anomalies in the master-slave scope and master job. The container is designed for handling anomalies in the parent coroutine

3.3, CoroutineExceptionHandler effect how

In AbstractCoroutine, the logic for handling exceptions is an empty implementation in the JobSupport interface by default.

protected open fun handleJobException(exception: Throwable): Boolean = false

The implementation logic is in StandaloneCoroutine (Builders.common.kt file)

private open class StandaloneCoroutine( parentContext: CoroutineContext, active: Boolean ) : AbstractCoroutine(parentContext, active) { override fun handleJobException(exception: Throwable): HandleCoroutineException (context, exception) return true}}Copy the code

The concrete implementation is as follows

//CoroutineExceptionHandlerImpl.kt private val handlers: List = ServiceLoader.load( CoroutineExceptionHandler::class.java, CoroutineExceptionHandler::class.java.classLoader ).iterator().asSequence().toList() internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // use additional extension 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 () currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception) }Copy the code

The above processing logic can be simply summarized as the following pseudocode

class StandardCoroutine(context: CoroutineContext) : AbstractCoroutine(context) { override fun handleJobException(e: Throwable): Boolean { context[CoroutineExceptionHandler]? .handleException(context, e) ? : Thread.currentThread().let { it.uncaughtExceptionHandler.uncaughtException(it, e) } return true } }Copy the code

Conclusion:

So by default, launch type coroutines for an uncaught exception is just print the exception stack information, if you use the CoroutineExceptionHandler, can only use a custom exception handling CoroutineExceptionHandler.

summary

1. The default scope of coroutines is coscope, and exceptions are propagated to parent coroutine processing in the form of coroutineScope or coroutineScope (Job()).

The exception is not propagated to the parent coroutine handling in the master-slave scope, which is the format for the CoroutineScope(container). The key is to override childCancelled()=false.

3, coroutines handle exceptions, if custom CoroutineExceptionHandler, by its processing, otherwise to system.

Finally, this article analyzes the exception handling from the coroutine scope as the entry point, look at the code process will also learn some kotlin clever syntax use; In addition, I only briefly analyzed the main line logic of exception processing, and I still need to continue to learn some details. I will conduct more detailed analysis next time. I hope this article is helpful to you, and welcome to exchange and study together.