Kotin collaboration from zero to one "Five"

Posted by Lyleyboy on Fri, 21 Jan 2022 00:53:26 +0100

The last article introduced the operation and recovery of the process, and this article introduces the parent-child process

What is a subprocess

Let's take an example here

The coroutine created by the first launch represents the dead parent coroutine, and the coroutine created by the second launch represents the child coroutine. Note that the first launch calls the this object method.

The second launch above calls the launch method of StandaloneCoroutine. StandaloneCoroutine represents a new collaboration, which also implements CoroutineScope. A subprocess can also have its own subprocess, which eventually forms a subprocess tree.

Relationship between father and son

1. Execution sequence of parent-child collaboration

     fun testChild() {
         var con= GlobalScope.launch {
              println("I'm the beginning of the father's Association")
              launch {
                  println("I'm zixie Cheng")
              }
              //After printing this sentence, the parent process may not have finished executing
              println("I'm the father. The process ends")
          }
         con.invokeOnCompletion {
             println("End of collaboration")
         }
    }

Take the above code as an example. Can you see the order of all printing.
First of all, the printing of println("I am a child process") and println("I am a parent process ends") depends entirely on the thread scheduling, so it is possible for anyone to print first. Here, suppose println("I am a parent process ends") is executed first, but after printing this sentence, The logic of the parent process has not been executed yet. In fact, it will wait for all the codes of the child processes to be executed before the parent process ends. At this time, the parent process will call the listener registered by invokeOnCompletion.

Why is the collaborative process so designed?
The problem of exception handling is mainly considered here. For example, an unhandled exception of the child process is thrown during the operation of the child process, but the parent process and the child process may run in two different threads, so it is meaningless to try to catch the exception of the child process in the parent process, Therefore, the parent process must wait for the child process to tell itself whether there is an exception after execution. After receiving the exception, the parent process will decide whether to handle the exception. If the parent process does not handle the exception, the exception may be thrown to the parent process of the parent process.

 public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
 
public typealias CompletionHandler = (cause: Throwable?) -> Unit

The parameter Throwable of CompletionHandler. This Throwable may be an exception thrown by the parent process itself or an exception passed by the child process to the parent process.

This part will be explained in the next article combined with the source code.

2 cancellation of cooperation

After a process is started, a child process can be started internally. When such a structured level exists, the cancellation of the parent process will cause the child process to immediately follow the cancellation. When the Job object returned after the process is started, you can listen for completion and cancellation events.

Establishment of parent-child relationship

Kotlin The child process does not directly hold the reference of the parent process, but is similar to the parent process registering a listener in the child process. We understand it in combination with the source code.

    public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        initParentJob()
        start(block, receiver, this)
    }
    internal fun initParentJob() {
        initParentJobInternal(parentContext[Job])
    }

The parentcontext value is the context of the parent collaboration process, that is, the context in the following figure

    internal fun initParentJobInternal(parent: Job?) {
        check(parentHandle == null)
        if (parent == null) {
            parentHandle = NonDisposableHandle
            return
        }
        //Ensure that the parent process has been started
        parent.start() // make sure the parent is started
        @Suppress("DEPRECATION")
        //Core method, establish parent-child relationship
        val handle = parent.attachChild(this)
        parentHandle = handle
        // now check our state _after_ registering (see tryFinalizeSimpleState order of actions)
        if (isCompleted) {
            handle.dispose()
            parentHandle = NonDisposableHandle // release it just in case, to aid GC
        }
    }

The actual type of parent is JobSupport

    @Suppress("OverridingDeprecatedMember")
    public final override fun attachChild(child: ChildJob): ChildHandle {
      
        return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(this, child).asHandler) as ChildHandle
    }

You can see that the parent-child relationship is actually established through invokeOnCompletion,

    public final override fun invokeOnCompletion(
        onCancelling: Boolean,
        invokeImmediately: Boolean,
        handler: CompletionHandler
    ): DisposableHandle {
        var nodeCache: JobNode<*>? = null
        loopOnState { state ->
            when (state) {
                is Empty -> { // EMPTY_X state -- no completion handlers
                    if (state.isActive) {
                        // try move to SINGLE state
                        val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
                        if (_state.compareAndSet(state, node)) return node
                    } else
                        promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
                }
                is Incomplete -> {
                    val list = state.list
                    if (list == null) { // SINGLE/SINGLE+
                        promoteSingleToNodeList(state as JobNode<*>)
                    } else {
                        var rootCause: Throwable? = null
                        var handle: DisposableHandle = NonDisposableHandle
                        if (onCancelling && state is Finishing) {
                            synchronized(state) {
                                // check if we are installing cancellation handler on job that is being cancelled
                                rootCause = state.rootCause // != null if cancelling job
                                // We add node to the list in two cases --- either the job is not being cancelled
                                // or we are adding a child to a coroutine that is not completing yet
                                if (rootCause == null || handler.isHandlerOf<ChildHandleNode>() && !state.isCompleting) {
                                    // Note: add node the list while holding lock on state (make sure it cannot change)
                                    val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
                                    if (!addLastAtomic(state, list, node)) return@loopOnState // retry
                                    // just return node if we don't have to invoke handler (not cancelling yet)
                                    if (rootCause == null) return node
                                    // otherwise handler is invoked immediately out of the synchronized section & handle returned
                                    handle = node
                                }
                            }
                        }
                        if (rootCause != null) {
                            // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
                            if (invokeImmediately) handler.invokeIt(rootCause)
                            return handle
                        } else {
                            val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
                            if (addLastAtomic(state, list, node)) return node
                        }
                    }
                }
                else -> { // is complete
                    // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
                    // because we play type tricks on Kotlin/JS and handler is not necessarily a function there
                    if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause)
                    return NonDisposableHandle
                }
            }
        }
    }

Let's first introduce loopOnState. We can see that loopOnState is an internal loop, but loopOnState is an inline method, so we only need the internal return of the block method, and the loopOnState will end.

    private inline fun loopOnState(block: (Any?) -> Unit): Nothing {
        while (true) {
            block(state)
        }
    }

The focus here is on the block method

    loopOnState { state ->
    when (state) {
        is Empty -> { 
            // The first case
          
        }
        is Incomplete -> {
         //The second case
            
        }
        else -> { // is complete
         //The third case
        }
    }
    }

There are three situations
1 Empty
This indicates the initial state of the collaboration. When we call iinvokeOnCompletion for the first time, we will take this branch. And the state will be set to invokeoncanceling, and
Invokeoncanceling inherits Incomplete again, so the second call to iinvokeOnCompletion will take the second branch.

2 Incomplete
The second call to iinvokeOnCompletion will take this branch. At this time, the state will be modified to NodeList, and the first node added before will be added to this list. Subsequent iinvokeOnCompletion added nodes will be added to this NodeList list

3else
At this point, it means that the whole collaboration process has completely ended, so handler will be called directly Invokeit actually calls the parentCancelled of the child process to notify the child process that the parent process is over,

OK, let's make a specific analysis

First kind

                    if (state.isActive) {
                         //isActive indicates whether the collaboration has been started. This is true
                        // If the collaboration is lazy, that is, the created collaboration is LazyStandaloneCoroutine
                        //Parrent. Above Start will also start the process and set isActive to true
                        
                        //makeNode will not post code. Here, change the state to INComplete, indicating that the collaboration is running
                        //And a node is registered through invokeOnCompletion.
                        val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
                        if (_state.compareAndSet(state, node)) return node
                    } else
                        promoteEmptyToNodeList(state) // 

Second

 This kind of is more complicated and more situations are considered.
                is Incomplete -> {
                    val list = state.list
                    if (list == null) { // SINGLE/SINGLE+
                       //
                        promoteSingleToNodeList(state as JobNode<*>)
                    } else {
                        var rootCause: Throwable? = null
                        var handle: DisposableHandle = NonDisposableHandle
                         //state is Finishing indicates that the running of the collaboration process is finished or the collaboration process is cancelled
                        if (onCancelling && state is Finishing) {
                            synchronized(state) {
                                // rootCause is the first exception of the coroutine
                                rootCause = state.rootCause // != null if cancelling job
                                 //handler. Ishandlerof < childhandlenode > this is true, so the following judgment can be simplified to
                                 //rootCause == null || !state.isCompletingļ¼Œ
                                 //rootCause == null indicates that the collaboration process ends normally
                                 //! state. Iscompleted indicates that the collaboration is cancelled
                                if (rootCause == null || handler.isHandlerOf<ChildHandleNode>() && !state.isCompleting) {
                                    // Note: add node the list while holding lock on state (make sure it cannot change)
                                    val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
                                    if (!addLastAtomic(state, list, node)) return@loopOnState // retry
                                    // just return node if we don't have to invoke handler (not cancelling yet)
                                    if (rootCause == null) return node
                                    // otherwise handler is invoked immediately out of the synchronized section & handle returned
                                    handle = node
                                }
                            }
                        }
                        if (rootCause != null) {
                            // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job
                            //invokeImmediately this is false
                            if (invokeImmediately) handler.invokeIt(rootCause)
                            return handle
                        } else {
                            //Normally, this branch will be used to add registered nodes in NodeList
                            val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
                            if (addLastAtomic(state, list, node)) return node
                        }
                    }
                }

In the first case, list == null indicates that iinvokeOnCompletion is called for the second time to add nodes. promoteSingleToNodeList creates a list Node as a new state, and then adds the first Node created previously to the list

The second case is list= Null indicates that iinvokeOnCompletion is called for the third time and more times. At this time, the state is NodeList. NodeList implements the Incomplete interface. Here, you will enter the list= Null branch. If (oncanceling & & state is Finishing) here oncanceling is true and state is Finishing. The process status is Finishing. It may be that the process ended normally or the process was cancelled.

After the execution of a collaboration, print("the last code of the collaboration") will set its status to Finishing, but iscompleted is true at this time, but the actual collaboration has not been actually executed. If there are child collaboration processes in the collaboration, the parent collaboration will wait for all child collaboration processes to be executed.
When we call the cancel method, the coroutine will also set its state to Finishing, but iscomplete is false In fact, the code block of the launch method may still be executing at this time,
At this time, although it is in the Finish state, you can actually continue to call iinvokeOnCompletion to add Node nodes, because there is a state after the Finish state and after all sub processes are executed. This state represents the return value of the process. If there is no return value, it is UNIT. If there is an exception, this state saves the exception. In this state, you can't add child coroutines at this time.

Third

That's what it says Finish After that, invokeIt This is to directly notify the child collaboration process to be created that the parent collaboration process has ended or cancelled.

Finally, it's too much to recommend an article for you https://blog.csdn.net/qfanmingyiq/article/details/105623638 , the addition of Node nodes is written in great detail and illustrated.

Topics: kotlin