Coroutines: First Things First
This series of blog posts delves into cancellations and exceptions in Kotlin coroutines. To avoid wasting memory and battery life, coroutine cancellation is critical; Proper exception handling is the key to a good user experience. As a basis for the other two parts of this series (Part 2: Cancellation, Part 3: exceptions), this blog defines some of the core concepts of coroutines, such as coroutine scope, Job, and coroutine context.
CoroutineScope (CoroutineScope)
CoroutineScope keeps track of any coroutines created using launch or Async (these are extension functions on CoroutineScope), and can cancel ongoing coroutines in this context at any point in time by calling scope.cancel(). Whenever we want to start and control the life cycle of a coroutine in our APP, we should create a CoroutineScope object. On the Android platform, there are already KTX libraries that provide CoroutineScope with some classes with life cycles. ViewModelScope and lifecycleScope. When CoroutineScope is created, it takes a CoroutineContext object as a construction parameter. You can create a new CoroutineScope and start a coroutine with it using the following code:
// Job and Dispatcher form a new CoroutineScope
// We will discuss this later
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// new coroutine
}
Copy the code
Job
A Job is a handle to a coroutine, and for each coroutine you launch via launch or Async, it returns a Job instance that uniquely identifies the coroutine and manages its life cycle. As in the code above, you can also pass the Job object to the CoroutineScope to maintain control over the coroutine life cycle.
CoroutineContext (CoroutineContext)
CoroutineContext is a set of elements that define the behavior of coroutines. It consists of:
- Job – Controls the life cycle of coroutines.
- CoroutineDispatcher – assigns coroutines to the appropriate threads.
- CoroutineName – The name of the coroutine, useful for debugging.
- CoroutineExceptionHandler – an uncaught exception handling, will be introduced in part 3 of this series.
When we create a new coroutine, what does its coroutine context have? We already know that a coroutine returns a new Job instance that allows us to control its life cycle, and that the rest of the elements will inherit from the parent of the coroutine (the parent coroutine or the scope that created it). Since coroutine scopes can create coroutines, and we can create more in coroutines, an implicit task hierarchy is formed. In the code below, in addition to creating a new coroutine using the CoroutineScope (CoroutineScope), you can see how to create more coroutines in coroutines:
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// CoroutineScope as the parent of the new coroutine
val result = async {
// A new coroutine created with the coroutine as the parent
}.await()
}
Copy the code
The root of this hierarchy is usually the topmost CoroutineScope. We can visualize the hierarchy as follows:
This hierarchy of tasks is the structured concurrency that Koltin coroutines pride themselves on — the parent coroutine can control and limit the life cycle of child threads, which inherit the coroutine context of the parent coroutine
Job life cycle
A Job can be in the New, Active, Completing, Completed, Canceling, or Canceled state. While we can’t access the state itself, we can access the attributes of the Job: isActive, isCancelled, and isCompleted.
job.cancel()
isActive = false, iscancel = true
isCompleted = true
An explanation of the parent CoroutineContext
In the task hierarchy of coroutines, each coroutine has a parent, which can be a coroutine scope or another coroutine. However, the parent context of coroutine inheritance may be different from the parent context itself, because the coroutine context is evaluated based on this formula
Parent context = default value + inherited context + parameter
- Some elements have default values, such as
Dispatchers.Default
Is the coroutine scheduler ( CoroutineDispatcher ), “coroutine” is the default value CoroutineName The default value of. - A coroutine inherits the coroutine scope or coroutine that created it.
- Parameters passed in the coroutine builder take precedence over elements in the inherited CoroutineContext.
Note: CoroutineContext can be combined using the + operator. Since CoroutineContext is a set of elements, the element to the right of the plus sign overwrites the same element to the left to create a new CoroutineContext. IO = (Dispatchers.IO, “name”)
The translator’s note: According to the CoroutineContext source code, CoroutineContext internally uses key-value pairs to maintain elements. The magic operation is that the keys of these key-value pairs correspond to associated objects of value types, and the values are instances of these types. So elements of the same type are unique in the same CoroutineContext.
Now that we know what the parent coroutine context of a new coroutine is, its actual coroutine context will be:
New CoroutineContext = parent CoroutineContext + Job()
If you create a new coroutine using the scope above, it looks like this:
valJob = scope.launch (dispatchers.io) {/ / new coroutines
}
Copy the code
So what will be the coroutine context and parent coroutine context for this new coroutine? See the answers below!
Dispatchers.IO
scope
Dispatchers.Main
launch
SupervisorJob
Job
SupervisorJob
CoroutineScope