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:

  1. start
  2. cancel
  3. 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. sleepJob1 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


  1. ChildHandle “childCancelled” means that child jobs are propagated upward when cancelled
  2. 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
  3. ChildHandleNode implements both ChildHandle and CompletionHandler, indicating that the node can propagate in both directions

The core methods are:

  1. JobSupport.initParentJobInternal(parent: Job?)
  2. Job.attachChild(child: ChildJob): ChildHandle
  3. 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