Originally written by Manuel Vivo
Cancellation and Exceptions in Coroutines (Part 1)
Translator: Jingpingcheng
Cancellation and exception handling of Coroutines is important for application memory management and power management; Handling exceptions properly is critical to improving user experience. Let’s take a look at the core concepts of Coroutines: CoroutineScope, Job, and CoroutineContext.
CoroutineScope
Coroutines can be created and managed by calling the CoroutineScope extension methods Launch and Async, and running Coroutines can be cancelled at any time through the scope.cancel() method.
In Android development, we typically take advantage of custom CoroutineScope with Lifecycle awareness in the KTX extension pack, such as viewModelScope and lifecycleScope.
When the CoroutineScope(Context: CoroutineContext) constructor is called to create a CoroutineScope, it receives a CoroutineContext argument.
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// new coroutine
}
Copy the code
The translator’s note: The + sign in the CoroutineScope constructor is confusing at first, but it’s actually an operator overload in CoroutineContext. If you’re using Android Studio, hold down CTRL + the left mouse button to jump to the implementation.
Job
Job is a handle to a coroutine. The CoroutineScope extension methods launch and Async both return a Job instance that uniquely identifies a Coroutine and handles the coroutine’s life cycle. You can also pass a Job instance directly into the CoroutineScope constructor and manage the Coroutine through that Job instance.
The Job instance in the CoroutineScope constructor is not the same as the Job instance returned by scope.launch. Let’s look at a piece of code
val parentJob = Job()
val scope = CoroutineScope(parentJob)
val job = scope.launch {// Or calling scope.async will do the same
}
val childJob: Job = parentJob.children.iterator().next()
println(childJob === job) //true
Copy the code
Verify that the Job instance returned by launching is the child instance of the Job instance in the CoroutineScope constructor. If we create two different Coroutines using the launch and async methods, they will both return a child instance of the same Job instance in the CoroutineScope constructor.
val parentJob = Job()
val scope = CoroutineScope(parentJob + Main)
val job = scope.launch {}
val asyncJob = scope.async {}
val jobIterator = parentJob.children.iterator()
val childJob1: Job = jobIterator.next()
println(childJob1 === job) // true
val childJob2: Job = jobIterator.next()
println(childJob2 === asyncJob) //true
Copy the code
CoroutineContext
A CoroutineContext consists of a series of elements that influence the behavior of a coroutine:
- Job – This has already been introduced.
- CoroutineDispatcher – a dispatcher for coroutine that dispatches coroutine to different threads for execution.
- CoroutineName – Give a name to your coroutine (similar to giving a name to Thread). This is useful for debugging.
- CoroutineExceptionHandler exception handling, this series of Part 3 will be described in detail.
A newly created coroutine’s CoroutineContext automatically inherits its parent coroutine’s CoroutineContext. Coroutines can continue to be created within coroutines, which means that coroutines can be nested, and nested coroutines are hierarchical (parent-child).
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// New coroutine that has CoroutineScope as a parent
val result = async {
// New coroutine that has the coroutine started by
// launch as a parent
}.await()
}
Copy the code
In general, the root node of the Coroutines hierarchy is created through the CoroutineScope, and all other nodes are children
Coroutines are executed in a task hierarchy. The parent can be either a CoroutineScope or another coroutine.
.The CoroutineScope constructor generates an instance of a Job by default, even if it is not passed.scope.cancel()
Method is called internallyjob.cancel()
methods
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
// If the incoming CoroutineContext does not contain a Job instance, a Job is created by default
ContextScope(if(context[Job] ! =null) context else context + Job())
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
// scope. Cancel () calls job.cancel() internally
valjob = coroutineContext[Job] ? : error("Scope cannot be cancelled because it does not have a job: $this")
job.cancel(cause)
}
Copy the code
Job lifecycle
The life cycle of a Job can be divided into New, Active, Completing, Completed, Cancelling and Cancelled. We cannot access the States directly, but we can access the isActive, isCancelled, and isCompleted properties of the Job.
Job lifecycle
If a coroutine fails to execute or is called job.cancel(), its state changes from active to Cancelling (isActive = false, isCancelled = true). Once all children have completed their task, coroutine state becomes Cancelled and isCompleted = true.
Parent CoroutineContext explained
The parent of a coroutine can be a CoroutineScope or another coroutine. The formula for CoroutineContext is
Parent context = Defaults + inherited CoroutineContext + arguments
- Some elements have Default values, such as dispatchers. Default for CoroutineDispatcher and “coroutine” for CoroutineName.
- CoroutineContext can be inherited from the CoroutineScope or from the parent coroutine that created it.
- Of course, you can pass elements to override the default elements or elements inherited from your parent Coroutine.
Note: CoroutineContext uses the + operator to combine elements. Elements to the right of the + operator overwrite elements to the left of the + operator. For example (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.
Every coroutine started by this CoroutineScope will have at least those elements in the CoroutineContext. CoroutineName is gray because it comes from the default values.
The CoroutineContext created by CoroutineScope is:
New coroutine context = parent CoroutineContext + Job()
The scope’s launch method is called at this point and the new Dispatchers are passed in
val job = scope.launch(Dispatchers.IO) {
// new coroutine
}
Copy the code
Dispatchers in CoroutineContext will now be overwritten from Dispatchers.Main to Dispatchers.IO, The Job returned by scope.launch(dispatchers.io) will also be a new Job instance.
The Job in the CoroutineContext and in the parent context will never be the same instance as a new coroutine always get a new instance of a Job
In Part 3 of this series, we’ll also be looking at different types of jobs, such as the SupervisorJob. Now that we’ve covered the basic concepts of Coroutines, we’ll continue exploring cancellation and exception handling of Coroutines in Parts 2 and 3.