1. Structured concurrency of coroutines
In the last article, I briefly introduced the case of Job Cancel method with two examples in different parent-child relationships. The code that looks very similar is executed with very different results. In this article, I introduce the concept of structured concurrency for jobs, that is, the data structure of a tree of parent jobs and child jobs. In this article, I introduce in detail how the Kotlin coroutine framework implements structured concurrency.
What is structured concurrency? In plain English, the collaboration between coroutines is organized and disciplined. If the relationship between coroutines is determined, then the cancel and exception handling between coroutines is unambiguous.
If an exception occurs at Job2, will the child Job, parent Job, and other child jobs of the parent Job be cancelled? The answer is that it depends. How does it depend?Because the algorithm is fixed, but the Job can behave differently). In the tree’s data structure, the Cancel event of any node has two propagation paths, propagating up to the parent node and down to the child node. Whether the parent or child node that receives the Cancel event cancels itself depends entirely on its internal implementation.
For example, suppose an exception occurs at Job2 and the coroutine is forced to cancel, and the event is propagated to Job0. So Job0 can be handled in two ways. Cancel yourself and cancel all of your child jobs, or ignore the event as if nothing happened.
Those of you who are familiar with coroutines will be aware that these two handles are designed for the Job and the Job it is designed for.
1.1 Job Processing Exceptions
The output is as follows: Job2 is started in Job mode. When joB2 is abnormal, job1 and job3 are cancelled.
2021-12-19 16:35:52.439 job1 start
2021-12-19 16:35:52.440 job2 start
2021-12-19 16:35:52.440 job3 start
Copy the code
1.2 SupervisorJob Handling anomalies
Job1 and job3 are not affected by joB2 anomalies, and the program runs as usual.
2021-12-19 16:44:30.832 job1 start 2021-12-19 16:44:30.833 job2 start 2021-12-19 16:44:30.833 job3 start 2021-12-19 2021-12-19 16:44:32.835 Job3 endCopy the code
The question, then, is why the container is handling the difference between the Job and the container?
2. Job basics
Launch the coroutine with the coroutinescope.launch method, returning a value of type Job. This article focuses on the following methods:
- start
- cancel
- invokeOnCompletion
2.1 start method
The start method starts a coroutine, similar to the thread. start method. Start the coroutine by default and the start method will be called by default.
2.2 cancel method
The cancel method is used to remove the coroutine and avoid wasting resources. Android developers are familiar with scenarios such as when an Activity is destroyed and the unfinished work should be cancelled. Cancel in a coroutine must have a hang start.
2.2.1 Using the Delay method
Observe that job1 was cancelled successfully because the delay method is decorated suspend, the point at which the coroutine is suspended.
2021-12-19 17:16:16.377 running in job2
Copy the code
2.2.2 Using thread. sleep
Job1 coroutine body does not call the suspend modification method and cannot be cancelled
2021-12-19 17:26:05.554 running in job2
2021-12-19 17:26:06.544 running in job1
Copy the code
2.2.3 Correction by Yield or ensureActive
To cancel job1, add yield or ensureActive to thread. sleep. Yield is itself a suspend function, and when the coroutine body is resumed at yield, it checks to see if the current coroutine has been canceled. EnsureActive cancels the current coroutine by raising a CancellationException.
2.2.4 CancellationException is a special Exception. It differs from Exception in that if a CancellationException is thrown in a coroutine, it does not affect all coroutines except child coroutines
CancellationException does not affect Job1 and job3, in contrast to 1.1 code 1/0 which throws a divisible by 0 exception.
2021-12-19 19:08:46.558 job1 start
2021-12-19 19:08:46.559 job2 start
2021-12-19 19:08:46.561 job3 start
2021-12-19 19:08:46.626 job4 start
2021-12-19 19:08:48.560 job1 end
2021-12-19 19:08:48.561 job3 end
Copy the code
2.3 invokeOnCompletion method
This method registers a callback for the Job and executes the callback when the Job is completed.
2021-12-19 19:19:39.278 job start
2021-12-19 19:19:41.280 job end
2021-12-19 19:19:41.281 job invokeOnCompletion
Copy the code
What if the coroutine is cancelled?
When the coroutine is cancelled, the callback is executed immediately.
2021-12-19 19:34:21.809 job start
2021-12-19 19:34:22.811 job invokeOnCompletion
Copy the code
Let’s look at the invokeOnCompletion source code
CompletionHandler is similar to the Callback in Java. When the Job completes, the callback is called, similar to Android’s OnClickListener. The CompletionHandler holds a ChildJob and registers it with the parent Job, so that the ChildJob can listen for the cancellation and completion events of the parent Job to propagate the event from top to bottom. If the child Job can directly or indirectly hold the reference of the parent Job, the event can be directly propagated from bottom to top when the child Job is cancelled. So the Cancel event is propagated both ways
The coroutine framework also establishes the tree relationship between father and son jobs through invokeOnCompletion method
3. Job Establishes the tree relationship
Let’s focus on a few classes with the red class icon above.
To highlight
- ChildHandle “childCancelled” means that child jobs are propagated upward when cancelled
- The invoke method of the CompletionHandler executes a callback to the ChildJob when the parent Job is cancelled, propagating down to the parentCancelled method of ChildJob
- ChildHandleNode implements both ChildHandle and CompletionHandler, indicating that the node can propagate in both directions
The core methods are:
- JobSupport.initParentJobInternal(parent: Job?)
- Job.attachChild(child: ChildJob): ChildHandle
- JobSupport.invokeOnCompletion( onCancelling: Boolean, invokeImmediately: Boolean, handler: CompletionHandler )
Set JobNode to the state property of the parent Job via CompletionHandler. The state property has the NodeList list, which is used to hold the callback list
At 1, if there is currently only one CompletionHandler, it is saved directly to state
At 2, the state property is promoted to a linked list if there are currently two completionHandlers
At 3, if there are currently more than two completionHandlers, the CompletionHandler is added directly to the end of the list.
PS this code looks more complicated, and it is also quite difficult to write, the first contact must be cloudy and foggy, time is limited, so I will write here first. I’ll write more about it later
In a nutshell
1. A Job establishes a relationship with its sub-jobs through the state NodeList object. When a Job is cancelled, a cancellation request is sent to the sub-jobs through this chain
2. The Job establishes a relationship with the parent Job using the parentHandle obtained in the initParentJobInternal method. When the child Job is cancelled, the parent Job sends a cancellation request using this reference
3. The bidirectional propagation mechanism of coroutine Job is determined by the algorithm of coroutine framework. It’s up to the Job implementation class to decide how to handle a cancellation request when it reaches a specific Job
This article explains how Job builds relationships. In the next post I’m going to show you how they handle two-way Cancel events on the relational link in conjunction with Job, Container Job, coroutineScope, container scope