Kotlin coroutine notes: CoroutineScope manages coroutines

         CoroutineScope is the key to realize the structured concurrency of coroutines. Using CoroutineScope, you can batch manage all coroutines under the same scope.

 

        CoroutineScope and Structured Concurrency

        launch and async are defined as extension functions of CoroutineScope. A CoroutineScope must be acquired before calling launch.

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
}

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

        Why should it be designed as an extension method?

        

fun main() {
    testCoroutinueScope()
}

private fun showScopeLog(any: Any?) {
    println("""$any Thread:${Thread.currentThread().name}""".trimIndent())
}

private fun testCoroutinueScope() {
    val scope = CoroutineScope(Job())
    scope.launch {
        launch {
            delay(1000000L)
            showScopeLog("Inner")
        }
        showScopeLog("Hello")
        delay(1000000L)
        showScopeLog("World") //不执行
    }

    scope.launch {
        launch {
            delay(1000000L)
            showScopeLog("Inner!!!")
        }
        showScopeLog("Hello!!!")
        delay(1000000L)
        showScopeLog("World!!!") //不执行
    }
    Thread.sleep(500L)
    scope.cancel()
}



Log:

Hello Thread:DefaultDispatcher-worker-1 @coroutine#1
Hello!!! Thread:DefaultDispatcher-worker-3 @coroutine#2

Process finished with exit code 0

 scope creates two top-level coroutines, and then inside the coroutine we use launch to create a sub-coroutine. Finally, 500 milliseconds are waited outside of the coroutine, and scope.cancel() is called. As a result, the four coroutines created earlier are all cancelled.

         The parent coroutine belongs to Scope, and the child coroutine belongs to the parent coroutine. As long as scope.cancel() is called, these 4 coroutines will be cancelled.

        

        The ability of CoroutineScope to manage coroutines comes from Job.

        How to establish the parent-child coroutine relationship? - How CoroutineScope manages coroutines through Job.

        CoroutineScope is an interface, why can its constructor be called to create a CoroutineScope object? Shouldn't the object keyword be used to create anonymous inner classes?

        Calling CoroutineScope() is not a constructor, but a top-level function.

private fun testCoroutinueScope() {
    val scope = CoroutineScope(Job())
    scope.launch {
        launch {
            delay(1000000L)
            showScopeLog("Inner")
        }
        showScopeLog("Hello")
        delay(1000000L)
        showScopeLog("World") //不执行
    }

    scope.launch {
        launch {
            delay(1000000L)
            showScopeLog("Inner!!!")
        }
        showScopeLog("Hello!!!")
        delay(1000000L)
        showScopeLog("World!!!") //不执行
    }
    Thread.sleep(500L)
    scope.cancel()
}

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
    // CoroutineScope is used intentionally for user-friendly representation
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)

         The function names in Kotlin follow the "camel case naming method" in most cases, but do not follow this naming method in some special cases. The top-level function CoroutineScope() above is actually a special case, because although it is an ordinary top-level function, its function is a "constructor". Similar usage, there is also the top-level function Job().

        In Kotlin, when a top-level function is used as a constructor, the first letter is capitalized.

        When creating CoroutineScope, if the incoming Context contains Job, then use it directly; if it does not contain Job, a new Job will be created. This means that every CoroutineScope object must have a Job object in its Context. It is no problem to change CoroutineScope(Job()) in the code to CoroutineScope().

        

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
}



private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

private class LazyStandaloneCoroutine(
    parentContext: CoroutineContext,
    block: suspend CoroutineScope.() -> Unit
) : StandaloneCoroutine(parentContext, active = false) {
    private val continuation = block.createCoroutineUnintercepted(this, this)

    override fun onStart() {
        continuation.startCoroutineCancellable(this)
    }
}

StandaloneCoroutine is a subclass of AbstractCoroutine, which represents the abstract class of coroutines. In addition, there is an initParentJob parameter, which is true, which means that after the coroutine is created, the parent-child relationship of the coroutine needs to be initialized. LazyStandaloneCoroutine is a subclass of StandaloneCoroutine, and its active parameter is false, which means creating a coroutine in a lazy loading manner.

public abstract class AbstractCoroutine<in T>(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
    ... ...
 init {
        /*
         * Setup parent-child relationship between the parent in the context and the current coroutine.
         * It may cause this coroutine to become _cancelling_ if the parent is already cancelled.
         * It is dangerous to install parent-child relationship here if the coroutine class
         * operates its state from within onCancelled or onCancelling
         * (with exceptions for rx integrations that can't have any parent)
         */
        if (initParentJob) initParentJob(parentContext[Job])
    }
}

AbstractCoroutine is a subclass of JobSupport. In the init{} code block, according to the initParentJob parameter, it is judged whether the parent-child relationship of the coroutine needs to be initialized. initParentJob is true, so the initParentJob() method here will definitely be executed, and the Job taken out by its parameter parentContext[Job] is actually the Job in the Scope.

 The initParentJob() method is a method in its parent class JobSupport.




public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob, SelectClause0 {
    final override val key: CoroutineContext.Key<*> get() = Job

    protected fun initParentJob(parent: Job?) {
        assert { parentHandle == null }
        
        if (parent == null) {
            parentHandle = NonDisposableHandle
            return
        }
        
        parent.start()
        @Suppress("DEPRECATION")
        
        val handle = parent.attachChild(this)
        parentHandle = handle

        if (isCompleted) {
            handle.dispose()
            parentHandle = NonDisposableHandle 
        }
    }
}


public interface Job : CoroutineContext.Element {
    public val children: Sequence<Job>   
    public fun attachChild(child: ChildJob): ChildHandle
}

There are three places to pay attention to in the above code, let's analyze it:

First judge whether the parent passed in is empty. If the parent is empty, it means that the current coroutine does not have a parent job, and there is no need to create a coroutine parent-child relationship.

Then make sure that the Job corresponding to the parent is started.

parent.attachChild(this) will add the current Job as a child Job of the parent. This is actually the key code to establish the parent-child relationship of the coroutine.

 How is the coroutine "structured cancellation"?

 The structured cancellation of coroutines is essentially the delivery of events.

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

         The cancel() method of CoroutineScope essentially calls Job.cancel() in it. And the specific implementation of this method is in JobSupport




public override fun cancel(cause: CancellationException?) {
    cancelInternal(cause ?: defaultCancellationException())
}

public open fun cancelInternal(cause: Throwable) {
    cancelImpl(cause)
}

internal fun cancelImpl(cause: Any?): Boolean {
    var finalState: Any? = COMPLETING_ALREADY
    if (onCancelComplete) {
        finalState = cancelMakeCompleting(cause)
        if (finalState === COMPLETING_WAITING_CHILDREN) return true
    }
    if (finalState === COMPLETING_ALREADY) {
        finalState = makeCancelling(cause)
    }
    return when {
        finalState === COMPLETING_ALREADY -> true
        finalState === COMPLETING_WAITING_CHILDREN -> true
        finalState === TOO_LATE_TO_CANCEL -> false
        else -> {
            afterCompletion(finalState)
            true
        }
    }
}
if (onCancelComplete) {        
        finalState = cancelMakeCompleting(cause)        
        if (finalState === COMPLETING_WAITING_CHILDREN) 
        return true   
}

job.cancel() eventually calls the JobSupport's cancelImpl() method. In the above code, onCancelComplete is a member property of Boolean type. Represents the current Job, whether there is a coroutine body that needs to be executed. In addition, since the Job in CoroutineScope is manually created and does not need to execute any coroutine code, it will be true. Continue to analyze the cancelMakeCompleting() method:

private fun cancelMakeCompleting(cause: Any?): Any? {
    loopOnState { state ->
        
        val finalState = tryMakeCompleting(state, proposedUpdate)
        if (finalState !== COMPLETING_RETRY) return finalState
    }
}

private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?): Any? {
    if (state !is Incomplete)
        return COMPLETING_ALREADY

       
        return COMPLETING_RETRY
    }

    return tryMakeCompletingSlowPath(state, proposedUpdate)
}

private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? {
    
    notifyRootCause?.let { notifyCancelling(list, it) }

    return finalizeFinishingState(finishing, proposedUpdate)
}

 cancelMakeCompleting() will call the tryMakeCompleting() method, which will eventually call the notifyCancelling() method in tryMakeCompletingSlowPath(). Therefore, it is the most critical code.

private fun notifyCancelling(list: NodeList, cause: Throwable) {
    onCancelling(cause)
    notifyHandlers<JobCancellingNode>(list, cause)
    cancelParent(cause)
}
  •  Notify child Job
notifyHandlers<JobCancellingNode>(list, cause)
  • Notify parent Job
 cancelParent(cause)

 

Notify the sub-Job process:

private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {
    var exception: Throwable? = null
    list.forEach<T> { node ->
        try {
            node.invoke(cause)
        } catch (ex: Throwable) {
            exception?.apply { addSuppressedThrowable(ex) } ?: run {
                exception =  CompletionHandlerException("Exception in completion handler $node for $this", ex)
            }
        }
    }
    exception?.let { handleOnCompletionException(it) }
}

 Traversing the sub-jobs of the current job and passing the canceled cause, the invoke() here will eventually call the invoke() method of ChildHandleNode:


internal class ChildHandleNode(
    @JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
    override val parent: Job get() = job
    override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
    override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}

public final override fun parentCancelled(parentJob: ParentJob) {
    cancelImpl(parentJob)
}

 The invoke() method of ChildHandleNode calls the parentCancelled() method, which eventually calls the cancelImpl() method. The entry function for Job cancellation. This is actually equivalent to doing a recursive call.

The process of notifying the parent Job:


private fun cancelParent(cause: Throwable): Boolean {
    if (isScopedCoroutine) return true

    val isCancellation = cause is CancellationException
    val parent = parentHandle

    if (parent === null || parent === NonDisposableHandle) {
        return isCancellation
    }
    
    return parent.childCancelled(cause) || isCancellation
}

 The return value of this function returns true to indicate that the parent coroutine handled the exception, and returns false to indicate that the parent coroutine did not handle the exception. This design pattern is similar to the chain of responsibility.


public open fun childCancelled(cause: Throwable): Boolean {
    if (cause is CancellationException) return true
    return cancelImpl(cause) && handlesException
}

 When the exception is CancellationException, the coroutine will perform special handling. In general, parent coroutines ignore cancellation exceptions from child coroutines. And if it is another exception, then the parent coroutine will respond to the cancellation of the child coroutine. The code will continue to call the cancelImpl() method recursively again.

Guess you like

Origin blog.csdn.net/zhangying1994/article/details/128377381