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:

Coroutines are executed in a task hierarchy. The parent can be a CoroutineScope or another coroutine

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 coroutine created by scope has at least these elements in its CoroutineContext. The value of CoroutineName is gray because it comes from the default

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!

The Job in the parent context is never the same instance as the Job in the context of the new coroutine, because the new coroutine always returns a new Job instance

Dispatchers.IO
scope
Dispatchers.Main
launch



SupervisorJob
Job
SupervisorJob
CoroutineScope