preface

This article is a record of my learning. It is written to deepen my memory and understanding, and also to share knowledge. The purpose of this article is to provide a slightly deeper and comprehensive knowledge of coroutines, as well as some examples of simple uses, which will not be discussed here, as there are better posts, which are linked to below, and the advanced uses of coroutines (hot data Channel, cold data Flow…). , this article mainly talk about coroutine a little deeper comprehensive knowledge.

Kotlin Coroutine profile

Coroutines in Kotlin provide a new way to handle concurrency, which you can use on the Android platform to simplify the asynchronous execution of code. Coroutines have been introduced since version 1.3 of Kotlin, but the concept has been around since the dawn of the programming world, with the earliest programming language using coroutines going back to 1967 with Simula.

The concept of coroutines has gained momentum over the past few years and has been adopted by many major programming languages, including Javascript, C#, Python, Ruby, and Go. Kotlin’s coroutines are based on established concepts from other languages.

On the Android platform, coroutines are used to solve two problems:

  • Processing Long running tasks, which often block the main thread;
  • Main-safety ensures that any suspend functions are safely called from the Main thread.

Kotlin Coroutine Version

Kotlin Version: 1.4.32

Coroutine Version: 1.4.3

Kotlin Coroutine ecological

Kotlin’s coroutine implementation is divided into two levels:

  • Infrastructure layer:

    The coroutine API of the standard library provides basic conceptual and semantic support for coroutines

  • Business framework layer Kotlin.Coroutines:

    Coroutine of the upper framework support, but also our daily development use of the library

Access to the Coroutine

dependencies {
    // Kotlin
    implementation "Org. Jetbrains. Kotlin: kotlin - stdlib: 1.4.32"

    // Coroutine core library
    implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.4.3"
    // Coroutine Android support library
    implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.4.3"
    // Coroutine Java8 support library
    implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines -- jdk8:1.4.3"
  
    // Lifecycle extends the encapsulation of coroutines
    implementation "Androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0." "
    implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0." "
    implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0." "
}
Copy the code

Coroutine for basic use

suspend

I’m not going to expand on suspend functions here, because there’s a better post out there, and that’s Of course The one by Chuck Line, which I originally learned from Chuck Line’s video. You can go to The Throw Line website to learn about suspend coroutines or anything else about coroutines. Here’s a link:

The hanging of a Kotlin coroutine? I skinned it today

Create coroutines

There are many ways to create coroutines, without extending the advanced uses of coroutines (hot data Channel, cold data Flow…) In the future, we may add or write a new article on coroutines. Here are two common ways to create coroutines:

  • CoroutineScope.launch()
  • CoroutineScope.async()

This is a common way to create coroutines. The Launch builder is good for doing “once and for all” work, meaning it can launch a new coroutine without returning the result to the caller; The Async builder starts a new coroutine and allows you to return result with a pending function called await. The big difference between Launch and async is how they handle exceptions. If async is used as the way to open the outermost coroutine, it expects to get the result (or exception) eventually by calling await, so by default it does not throw an exception. This means that if a new outermost coroutine is launched with async instead of await, it silently drops the exception.

CoroutineScope.launch()

Go directly to the code:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

    /** * Use the official library's MainScope() to get a coroutine scope to create coroutines */
    private val mScope = MainScope()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Create a coroutine whose default scheduling mode is Main. This means that the thread environment of the coroutine is Main thread
        val job1 = mScope.launch {
            // This is the body of the coroutine

            Delay is a suspend function
            // The thread on which the coroutine is located will not block for 1000 milliseconds
            // The coroutine will hand over the execution of the thread, the thread will do what it does, when the time is up to resume to continue to execute down
            delay(1000)}// Create a coroutine with the specified scheduling mode. This coroutine runs on the IO thread
        val job2 = mScope.launch(Dispatchers.IO) {

            // This is IO thread mode

            // Cut the thread environment of the coroutine to the specified scheduling mode Main
            withContext(Dispatchers.Main) {
                // Now this is the Main thread where you can perform UI operations}}// Let's look directly at an example: fetching data from the network and updating the UI
        // This example does not block the main thread
        mScope.launch(Dispatchers.IO) {
            // Executing the getUserInfo method cuts the thread to IO
            val userInfo = getUserInfo()
            // After getting the data, switch to the Main thread to update the UI
            withContext(Dispatchers.Main) {
                / / update the UI}}}/** * Obtain user information. This function simulates IO obtaining data *@return String
     */
    private suspend fun getUserInfo(a): String {
        return withContext(Dispatchers.IO) {
            delay(2000)
            "Kotlin"}}override fun onDestroy(a) {
        super.onDestroy()
        // Remove coroutine to prevent coroutine leakage. If lifecycleScope is used, manual cancellation is not required
        mScope.cancel()
    }
}
Copy the code

In the code above, given some code examples, the simple use of coroutines is so simple that you don’t even have to worry about anything else, just remember to cancel the coroutine, and if you use lifecycleScope or viewModelScope you don’t even have to cancel it yourself. When the interface or ViewModel is destroyed, it will automatically cancel the coroutine for you. Use coroutines only need to create, cut threads, understand the four scheduling patterns, the basic ok, basic development has met.

CoroutineScope.async()

Async is mainly used to get the return value and concurrency.

fun asyncTest(a) {
    mScope.launch {
        // Start an IO mode thread and return a Deferred. The Deferred can be used to get the return value
        // At this point, the code will open a new coroutine and then execute the body of the coroutine. The parent coroutine's code will follow
        val deferred = async(Dispatchers.IO) {
            // Simulation time
            delay(2000)
            // Return a value
            "Quyunshuo"
        }
        // Wait for async to complete its execution and get the return value. This does not block the thread but suspends the thread's execution
        // After the async coroutine body finishes executing, the coroutine will resume execution
        val date = deferred.await()
    }
}
Copy the code

The above code mainly shows the return value function of async and needs to be used in conjunction with the await() suspend function

The following shows the concurrency capabilities of async:

fun asyncTest2(a) {
    mScope.launch {
        // Here is a requirement to request all five interfaces simultaneously and concatenate the return values

        val job1 = async {
            / / request 1
            delay(5000)
            "1"
        }
        val job2 = async {
            2 / / request
            delay(5000)
            "2"
        }
        val job3 = async {
            3 / / request
            delay(5000)
            "3"
        }
        val job4 = async {
            / / request 4
            delay(5000)
            "4"
        }
        val job5 = async {
            / / request 5
            delay(5000)
            "5"
        }

        // At this point in the code, five requests are already executing at the same time
        // Wait for each job to complete and merge the results
        Log.d(
            "TAG"."asyncTest2: ${job1.await()} ${job2.await()} ${job3.await()} ${job4.await()} ${job5.await()}"
        )

        // Since we set the simulation time to 5000 milliseconds, when job1 is finished, all other jobs are finished}}Copy the code

The above code is a simple example of concurrency, and if it feels very simple, the advantages of coroutines are immediately obvious.

This is the most basic coroutines, about scope, more recommendation is used in the UI components LifecycleOwner. LifecycleScope, using ViewModel in ViewModel. ViewModelScope

The depth of the Coroutine

In fact, the simple use of Coroutine has met most of the daily development needs, but we need to have a comprehensive understanding of Coroutine, in order to troubleshoot problems and customize the scenario, let’s start with a basic function, this function is launch{} :

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,	// Coroutine context
    start: CoroutineStart = CoroutineStart.DEFAULT,			// Coroutine start mode
    block: suspend CoroutineScope. () - >Unit						// Logic that runs in coroutines
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
Copy the code

Above is the definition of the launch function, which takes the form of the CoroutineScope extension function. The parameters are: CoroutineContext CoroutineContext CoroutineStart CoroutineStart CoroutineContext CoroutineContext CoroutineContext coroutindispatcher CoroutineName CoroutineName CoroutineName CoroutineContext CoroutineContext CoroutineContext CoroutineContext CoroutineContext CoroutineContext CoroutineContext CoroutineContext CoroutineContext CoroutineContext Here we will introduce them one by one: CoroutineContext, Job, CoroutineDispatcher, CoroutineStart, CoroutineScope.

CoroutineContext – CoroutineContext

CoroutineContext is the context of a coroutine. It is a basic building unit of a Kotlin coroutine. Clever use of coroutine context is critical to correct threading behavior, lifecycle, exceptions, and debugging. It contains user-defined sets of data that are closely related to coroutines. It is a collection of indexed Element instances. The indexed set is similar to a data structure between a set and a map. Each element has a unique Key in the set. When multiple elements have the same key reference, they belong to the same element in the collection. It consists of the following items:

  • Job: Controls the life cycle of coroutines;
  • CoroutineDispatcher: Dispatches tasks to appropriate threads;
  • CoroutineName: The name of a coroutine, useful for debugging;
  • CoroutineExceptionHandler: has not been caught exception handling.

CoroutineContext has two very important elements — the Job, which is the current Coroutine instance, and the Dispatcher, which determines the thread on which the Coroutine is executing. You can also add CoroutineName, used for debugging, add CoroutineExceptionHandler used to catch exceptions, they all realized the Element interface. Here’s an example:

fun main(a) {
    val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
    println("$coroutineContext.${coroutineContext[CoroutineName]}")
    val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
    println("$newCoroutineContext")}Copy the code

The output is as follows:

[JobImpl{Active}@7eda2dbb, CoroutineName(myContext), Dispatchers.Default],CoroutineName(myContext)
[JobImpl{Active}@7eda2dbb, Dispatchers.Default]
Copy the code

The CoroutineContext interface is defined as follows:

public interface CoroutineContext {
    
    public operator fun <E : Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R.Element) - >R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext{... }public fun minusKey(key: Key< * >): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {... }
}
Copy the code

CoroutineContext defines four core operations:

  • Operator to get

    The Element can be retrieved by the key. Since this is a GET operator, it can be accessed as a map element using context[key] brackets.

  • Operator plus

    Similar to the set. plus extension function, it returns a new context object containing all the elements in both of them. If duplicate elements are encountered, the Element to the right of the + sign is used instead of the one to the left. The + operator can easily be used in context, but there is one important thing to be careful about — the order in which they are combined, because the + operator is asymmetric.

  • fun fold(initial: R, operation: (R, Element) -> R): R

    Similar to the collection.fold extension function, it provides the ability to traverse all elements in the current context.

  • fun minusKey(key: Key<*>): CoroutineContext

    Returns a context containing the elements in that context, but not the elements with the specified key.

The EmptyCoroutineContext object can be used in cases where a context does not hold any elements. As you can expect, adding this object to another context has no effect on it.

At the task level, each coroutine has a parent object, either a CoroutineScope or another Coroutine. However, the CoroutineContext of the parent coroutine is not the same as the CoroutineContext of the parent coroutine because of the following formula:

Parent context = default value + inherited CoroutineContext + parameter

Among them:

  • Some elements contain Default values: Dispatchers.Default is the Default CoroutineDispatcher, and “coroutine” as the Default CoroutineName;
  • The inherited CoroutineContext is the CoroutineScope or CoroutineContext of its parent coroutine;
  • Arguments passed to the coroutine Builder take precedence over inherited context arguments, so the corresponding parameter values are overwritten.

Note: CoroutineContext can be merged using the “+” operator. Since CoroutineContext is made up of a set of elements, the elements to the right of the plus sign overwrite the elements to the left of the plus sign to form the newly created CoroutineContext. For example, (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.

Job & Deferred – Jobs

Job is used to process coroutines. For each coroutine that is created (via launch or async), it returns a Job instance that uniquely identifies the coroutine and manages its life cycle

The coroutinescope. launch function returns a Job object that represents an asynchronous task. Jobs have a life cycle and can be cancelled. A Job can contain multiple child jobs. When the parent Job is cancelled, all child jobs are automatically cancelled. When a child Job is cancelled or an exception occurs, the parent Job is also cancelled.

In addition to creating the Job object via coroutinescope.launch, you can also create the object via the Job() factory method. By default, the parent Job is cancelled when the child Job fails. This is the default behavior that can be modified by the SupervisorJob.

A parent Job with multiple child jobs waits for all child jobs to complete (or cancel) before it completes

The state of the Job

A task can have a list of states: New, Active, Completing, Completed, Cancelling, and Cancelled. While we cannot access these states directly, we can access the Job properties isActive, isCancelled, and isCompleted.

If the coroutine isActive, an error in running the coroutine or a call to job.cancel() will make the current task Cancelling (isActive = false, isCancelled = true). When all subcoroutines have been completed, the coroutine enters the Cancelled state where isCompleted = true.

State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false
                                      wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+
Copy the code

Common functions for Job

These functions are thread-safe, so they can be called directly from other Coroutines.

  • fun start(): Boolean

    This function is called to start the Coroutine, and returns true if the Coroutine has not yet been executed, or false if the Coroutine has been or has completed execution

  • fun cancel(cause: CancellationException? = null)

    Cancel this job by an optional cancellation reason. The cause can be used to specify an error message or to provide additional details about the cause of the cancellation for debugging purposes.

  • fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

    This function sets a completion notification for the Job, which is executed synchronously when the Job completes. The notification object type of the callback is: TypeAlias CompletionHandler = (Cause: Throwable?) -> Unit.

    The CompletionHandler parameter represents how the Job was executed. There are three causes: – If the Job is executed normally, the cause parameter is null. – If the Job is cancelled normally, the cause parameter is CancellationException. This situation should not be treated as an error; it is a normal cancellation of the task. There is generally no need to record this in the error log. – In other cases, the Job fails to be executed.

    This function returns a value of DisposableHandle, so you can call DisposableHandle.dispose to cancel the monitoring if you no longer need to monitor the Job’s completion. If the Job is finished, you do not need to call the Dispose function and the listener is automatically cancelled.

  • suspend fun join()

    Unlike the previous three functions, the join function is a suspend function. So it can only be called inside a Coroutine.

    This function pauses the current Coroutine until it completes. So the Job function is used to wait for the Job to complete in another Coroutine. When the Job execution is complete, the Job. Join function resumes, and the Job task is in the activie state, while the Coroutine that calls the Job. Join continues to be in the Activie state.

    Note that a job is not complete until all of its children have completed

    The suspension of this function can be cancelled, and always check to see if the calling Coroutine’s Job is cancelled. This function raises a CancellationException if the Job that called Coroutine is cancelled or completed when this suspended function is called or suspended.

Deferred

public interface Deferred<out T> : Job {

    public val onAwait: SelectClause1<T>

    public suspend fun await(a): T

    @ExperimentalCoroutinesApi
    public fun getCompleted(a): T

    @ExperimentalCoroutinesApi
    public fun getCompletionExceptionOrNull(a): Throwable?
}
Copy the code

By creating a Coroutine with async, you can get a return Deferred. The Deferred interface inherits from the Job interface and provides an additional method to get the Coroutine’s return result. Because Deferred inherits from the Job interface, job-related content is also applicable to Deferred. Deferred provides three additional functions to handle operations related to Coroutine execution results.

  • suspend fun await(): T

    Wait for the Coroutine to complete and return the result.

  • fun getCompleted(): T

    Used to get the results of Coroutine execution. An IllegalStateException is thrown if the Coroutine has not completed, and an exception is thrown if the task is cancelled. So before executing this function, you can use isCompleted to determine whether the current task isCompleted.

  • fun getCompletionExceptionOrNull(): Throwable?

    Obtain Coroutine exception information about the completed task. If the task is completed normally, no exception information is displayed and NULL is returned. Calling this function also throws an IllegalStateException if it is not already in the completed state, to determine whether the current task isCompleted by saying isCompleted.

SupervisorJob

SupervisorJob is a top-level function and is defined as follows:

/** * Creates a _supervisor_ job object in an active state. * Children of a supervisor job can fail independently of each other. * * A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, * so a supervisor can implement a custom policy for handling failures of its children: * * * A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context. * * A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value. * * If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its * parent fails or is cancelled. All  this supervisor's children are cancelled in this case, too. The invocation of * [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job  also cancels parent. * *@param parent an optional parent job.
 */
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
Copy the code

This function creates a supervisor job in the active state. As mentioned earlier, jobs have a parent-child relationship, and if a child Job fails, the parent Job automatically fails. This default behavior may not be what we expect. For example, there are two sub-jobs in the Activity that fetch comment content and author information for an article. If one of them fails, we don’t want the parent Job to be canceled automatically, which would cause the other child Job to be canceled as well. The SupervisorJob is designed to run on its SupervisorJob, and it is designed to run on its SupervisorJob. It is designed to run on its SupervisorJob, and it is designed to run on its SupervisorJob. It is designed to run on its SupervisorJob, and it is designed to run on its SupervisorJob. SupervisorJob(parent:Job?) Has a parent argument, and if specified, the returned Job is the child Job of the parent argument. If the Parent Job fails or is cancelled, the Supervisor Job will also be cancelled. When a Supervisor Job is cancelled, all sub-jobs of the Supervisor Job are also cancelled.

The MainScope() implementation is SupervisorJob and a Main Dispatcher:

/** * Creates the main [CoroutineScope] for UI components. * * Example of use: * ``` * class MyAndroidActivity { * private val scope = MainScope() * * override fun onDestroy() { * super.onDestroy() *  scope.cancel() * } * } * ``` * * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements. * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator: * `val scope = MainScope() + CoroutineName("MyActivity")`. */
@Suppress("FunctionName")
public fun MainScope(a): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code

But SupervisorJob is easy to be misunderstood, it and coroutines Job type and exception handling, son coroutines belongs to domain where there are lots of confusing, specific exception handling can see Google this article: the cancellation, and abnormal | coroutines exception handling, rounding

CoroutineDispatcher – Dispatcher

The CoroutineDispatcher defines the thread of execution of the Coroutine. CoroutineDispatcher can restrict Coroutine execution to a thread, assign it to a thread pool, or restrict its execution to no thread.

CoroutineDispatcher is an abstract class from which all Dispatchers should inherit their functionality. Dispatchers is a helper class in the standard library that helps us to switch threads. It can be simply understood as a thread pool. It is implemented as follows:

  • Dispatchers.Default

    The default scheduler, suitable for background computing, is a CPU-intensive task scheduler. If the Coroutine is created without a dispatcher specified, this is the default value. The Default Dispatcher uses a shared background thread pool to run its tasks. Note that it is the same as the IO shared thread pool, but with a different maximum concurrency limit.

  • Dispatchers.IO

    As the name implies, this is used to perform blocking IO operations. It shares a shared thread pool with Default to perform tasks within it. Depending on the number of concurrent tasks, additional threads are created as needed, and unneeded threads are released when the task finishes.

  • Dispatchers.Unconfined

    Because the Dispatchers.Unconfined thread pool is not defined, the thread is enabled by default. After the first suspension point is encountered, the thread that called resume decides which thread to resume the coroutine.

  • Dispatchers. Main:

    The specified thread of execution is the main thread, which on Android is the UI thread

Since the child Coroutine inherits the context of the parent Coroutine, we usually set a Dispatcher on the parent Coroutine for ease of use, and then all the child coroutines automatically use this Dispatcher.

CoroutineStart – CoroutineStart mode

  • CoroutineStart.DEFAULT:

    The coroutine is scheduled as soon as it is created. If the coroutine is cancelled before the scheduled time, it will go directly into the cancelled response state

    Although it is scheduled immediately, it may be cancelled before execution

  • CoroutineStart.ATOMIC:

    Scheduling begins immediately after coroutine creation. Coroutine execution does not respond to cancellation until the first suspension point

    Although it is immediate scheduling, it combines the scheduling and execution steps into one, and as its name suggests, it guarantees that scheduling and execution are atomic operations, so the coroutine must execute as well

  • CoroutineStart.LAZY:

    Scheduling begins as long as the coroutine is needed, including active calls to the coroutine’s start, join, or await functions. If cancelled before scheduling, the coroutine goes directly to the exception end state

  • CoroutineStart.UNDISPATCHED:

    The coroutine is created and executed immediately in the current function call stack until it hits the first true hang point

    Is immediately executed, so the coroutine must execute

These startup modes are designed for specific scenarios. In business development practice, the two startup modes of DEFAULT and LAZY are usually sufficient

CoroutineScope – CoroutineScope

To define a coroutine you must specify its CoroutineScope. CoroutineScope tracks coroutines even if they are suspended. Unlike a Dispatcher, CoroutineScope doesn’t run coroutines; it just makes sure you don’t lose track of them. To ensure that all coroutines are tracked, Kotlin does not allow new coroutines to be launched without using the CoroutineScope. CoroutineScope can be seen as a lightweight version of the superpowered ExecutorService. CoroutineScope tracks all coroutines, and it can also cancel all coroutines started by it. This can be useful in Android development, such as the ability to stop execution of coroutines when the user leaves the interface.

Just because coroutines are lightweight threads doesn’t mean they don’t consume system resources. When the asynchronous operation is time-consuming, or when the asynchronous operation goes wrong, the Coroutine needs to be cancelled to free up system resources. In the Android environment, usually the Coroutine launched on each interface (Activity, Fragment, etc.) is only meaningful on that interface. If the user exits the interface while waiting for the Coroutine to execute, Further execution of the Coroutine may not be necessary. The Coroutine also needs to be executed in the proper context, otherwise errors will occur, such as accessing the View from a non-UI thread. Therefore, when Coroutine is designed, it is required to be executed within a Scope, so that when the Scope is cancelled, all the sub-coroutine in it will be automatically cancelled. To use a Coroutine, you must first create a corresponding CoroutineScope.

CoroutineScope interface

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
Copy the code

CoroutineScope simply defines a new Coroutine execution Scope. Each Coroutine Builder is an extension function of CoroutineScope and automatically inherits coroutineContext from the current Scope.

Classification and rules of conduct

The official framework also provides scope when implementing composite coroutines, which is used to specify the parent-child relationship between the written and the propagation behavior for cancellation or exception handling. There are three types of scope:

  • Top-level scope

    The scope of a coroutine that has no parent coroutine is the top-level scope.

  • Cooperative scope

    The new coroutine is a subcoroutine of the existing coroutine. In this case, the scope of the subcoroutine is the cooperative scope by default. In this case, any uncaught exceptions thrown by the child coroutine are passed to the parent coroutine, and the parent coroutine is cancelled.

  • Master-slave scope

    Consistent with the cooperative scope in the parent-child relationship of coroutines, the difference is that when an uncaught exception occurs in a coroutine under this scope, the exception is not passed upward to the parent coroutine.

In addition to the behaviors mentioned in the three scopes, the following rules exist between parent and child coroutines:

  • When a parent coroutine is canceled, all child coroutines are canceled. Since there is a parent-child coroutine relationship in both the synergy scope and the master-slave scope, this rule applies to both.
  • The parent coroutine waits for the child coroutine to complete before finally entering the complete state, regardless of whether its own coroutine body has finished executing.
  • Subcoroutines inherit elements in the coroutine context of their parent, if they are identicalkey, the correspondingkey, the effect of coverage is only valid within its own scope.

Common scope

The official libraries give us some scopes to use directly, and Android’s Lifecycle Ktx library also encapsulates more useful scopes. Let’s take a look at the various scopes

GlobalScope – Not recommended

public object GlobalScope : CoroutineScope {
    /** * Returns [EmptyCoroutineContext]. */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}
Copy the code

The context is EmptyCoroutineContext, which is an empty context that does not contain any jobs. This scope is often used as example code because the GlobalScope object is not associated with the application lifecycle component. You need to manage the Coroutine created by GlobalScope, and the life cycle of GlobalScope is process level, so we generally do not recommend using GlobalScope to create Coroutine.

RunBlocking {} – Mainly used for testing

/** * Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion. * This function should not be used from a coroutine. It is designed to bridge regular blocking code * to libraries that are written in suspending style, to be used in `main` functions and in tests. * * The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations * in this blocked thread until the completion of this coroutine. * See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`. * * When  [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of * the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`, * then this invocation uses the outer event loop. * * If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and * this `runBlocking` invocation throws [InterruptedException]. * * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available *  for a newly created coroutine. * *@param context the context of the coroutine. The default value is an event loop on the current thread.
 * @param block the coroutine code.
 */
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope. () - >T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)? .takeIf { it.shouldBeProcessedFromContext() } ? : ThreadLocalEventLoop.currentOrNull() newContext = GlobalScope.newCoroutineContext(context) }val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}
Copy the code

This is a top-level function, we can get some information from the source code comments, and run a new coroutines and blocking the current interruptible thread until coroutines completes, the function should not a collaborators in use process, this function is designed for bridge common block of code to hang style (suspending style) written in library, For main functions and tests. This function is mainly used for testing, not for daily development, and the coroutine blocks the current thread until the coroutine body completes execution.

MainScope() – Available for development

/** * Creates the main [CoroutineScope] for UI components. * * Example of use: * ``` * class MyAndroidActivity { * private val scope = MainScope() * * override fun onDestroy() { * super.onDestroy() *  scope.cancel() * } * } * ``` * * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements. * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator: * `val scope = MainScope() + CoroutineName("MyActivity")`. */
@Suppress("FunctionName")
public fun MainScope(a): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code

SupervisorJob() + Dispatchers.Main is used in the Activity/Fragment format. It is usually used in the Activity/Fragment format. Fun coroutinescope. cancel(cause: CancellationException? = null) to cancel the coroutine, this is the official library can be used in the development of a top-level function used to get the scope, use examples in the official library code comments have been given, the above source code also has, it is very convenient to use.

LifecycleOwner lifecycleScope – it is recommended to use

/** * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
Copy the code

This extended property is the Lifecycle aware coroutine scope provided by Android’s Lifecycle Ktx library. It is bound to LifecycleOwner’s Lifecycle and will be removed when Lifecycle is destroyed. This is the recommended scope for Activity/Fragment because it is bound to the current UI component lifecycle, and when the interface is destroyed, the coroutine scope is removed without coroutine leakage, as is viewModel.ViewModelScope below.

Viewmodel. viewModelScope – Recommended

/** * [CoroutineScope] tied to this [ViewModel]. * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] */
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if(scope ! =null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }
Copy the code

The extended attributes and mentioned above LifecycleOwner. LifecycleScope basic consistent, it is an extension of the ViewModel properties, also from Android’s Lifecycle Ktx library, it can be cancelled automatically when the ViewModel destroyed, It also does not cause coroutine leakage. The extension of the scope of the property returns the context is also SupervisorJob () + Dispatchers. Main. Immediate

coroutineScope & supervisorScope

/** * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope. * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides * context's [Job] with [SupervisorJob]. * * A failure of a child does not cause this scope to fail and does not affect its other children, * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details. * A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children, * but does not cancel parent job. */
public suspend fun <R> supervisorScope(block: suspend CoroutineScope. () - >R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}



/** * Creates a [CoroutineScope] and calls the specified suspend block with this scope. * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides * the context's [Job]. * * This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails, * this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]). * This function returns as soon as the given block and all its children coroutines are completed. * A usage example of a scope looks like this: * * ``` * suspend fun showSomeData() = coroutineScope { * val data = async(Dispatchers.IO) { // <- extension on current scope * ... load some UI data for the Main thread ... * } * * withContext(Dispatchers.Main) { * doSomeWork() * val result = data.await() * display(result) * } * } * ``` * * The scope in this example has the following semantics: * 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI. * 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception. * 3) If the outer scope of `showSomeData`  is cancelled, both started `async` and `withContext` blocks are cancelled. * 4) If the `async` block fails, `withContext` will be cancelled. * * The method may throw a [CancellationException] if the current job was cancelled externally * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope * (for  example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope). */
public suspend fun <R> coroutineScope(block: suspend CoroutineScope. () - >R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}
Copy the code

First, both of these functions are suspended functions that need to be run inside a coroutine or a suspended function. SupervisorScope belongs to the master-slave scope, inherits the father coroutines context, its characteristic is the exception will not affect the parent child coroutines coroutines, its design scenarios for coroutines for the task of independent peer entities, such as a downloader, each child coroutines is a download task, when a download task anomalies, It should not affect other download tasks. CoroutineScope and supervisorScope both return to a scope. The difference between the two is exception propagation: exceptions inside the coroutineScope are propagated upward, exceptions uncaught by subcoroutines are passed upward to the parent coroutine, and any subcoroutine exception that exits causes the entire coroutine to exit. SupervisorScope internal abnormal won’t spread upward, a child coroutines abnormal exit, coroutines does not affect the father and brother coroutines running.

Cancellations and exceptions of coroutines

A normal coroutine that generates an unhandled exception propagates that exception to its parent coroutine, which then cancles all child coroutines, cancles itself, and continues passing the exception upward. Here’s an official illustration of this process:

Sometimes this is not what we want. We prefer that one coroutine generates an exception that does not affect the execution of the other coroutines. We also mentioned some solutions in the previous article, but let’s try them out.

Use SupervisorJob * *

We also explained this top-level function in the previous article, so how to use it? Go directly to the code:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

    /** * Use the official library's MainScope() to get a coroutine scope to create coroutines */
    private val mScope = MainScope()

    companion object {
        const val TAG = "Kotlin Coroutine"
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mScope.launch(Dispatchers.Default) {
            delay(500)
            Log.e(TAG, "Child 1")
        }

        mScope.launch(Dispatchers.Default) {
            delay(1000)
            Log.e(TAG, "Child 2")
            throw RuntimeException("--> RuntimeException <--")
        }

        mScope.launch(Dispatchers.Default) {
            delay(1500)
            Log.e(TAG, "Child 3"}}} Print E/Kotlin Coroutine: Child1
E/Kotlin Coroutine: Child 2
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3
    Process: com.quyunshuo.kotlincoroutine, PID: 24240
    java.lang.RuntimeException: --> RuntimeException <--
        at com.quyunshuo.kotlincoroutine.MainActivity$onCreate$2.invokeSuspend(MainActivity.kt:31)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
E/Kotlin Coroutine: Child 3
Copy the code

MainScope() is designed to run on its SupervisorJob. The result is that Child 3 executes normally when Child 2 throws an exception, but the program crashes because we didn’t handle the exception. Let’s refine our code

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    mScope.launch(Dispatchers.Default) {
        delay(500)
        Log.e(TAG, "Child 1")}// Added exception handling to the context of Child 2
    mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.e(TAG, "CoroutineExceptionHandler: $throwable")
    }) {
        delay(1000)
        Log.e(TAG, "Child 2")
        throw RuntimeException("--> RuntimeException <--")
    }

    mScope.launch(Dispatchers.Default) {
        delay(1500)
        Log.e(TAG, "Child 3"}} Output: E/Kotlin Coroutine: Child1
E/Kotlin Coroutine: Child 2
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3
Copy the code

This time, the program doesn’t crash and the exception handling prints out, which is what we want. The only thing it is supposed to be handling is that these subcoroutines are supervisorjobs, and when they have subcoroutines, they’re not supposed to be the supervisorjobs they’re supposed to be. We use an official diagram to explain this relationship:

This diagram can be said to be very intuitive, or the official 🐂. When the new coroutine is created, it generates a new Job instance to replace the SupervisorJob.

Using supervisorScope

This scope we have mentioned in this paper, using supervisorScope can also achieve the result that we want to, the code:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "Kotlin Coroutine"
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val scope = CoroutineScope(Job() + Dispatchers.Default)

        scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(TAG, "CoroutineExceptionHandler: $throwable")
        }) {
            supervisorScope {
                launch {
                    delay(500)
                    Log.e(TAG, "Child 1 ")
                }
                launch {
                    delay(1000)
                    Log.e(TAG, "Child 2 ")
                    throw  RuntimeException("--> RuntimeException <--")
                }
                launch {
                    delay(1500)
                    Log.e(TAG, "Child 3 ")}}}}} Output: E/Kotlin Coroutine: Child1 
E/Kotlin Coroutine: Child 2 
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3 
Copy the code

You can see the supervisorScope is doing exactly what you want, but if you replace it with a coroutineScope, it won’t. Finally, I will show you the official picture:

conclusion

So far the article has ended, this article is mainly I learn coroutine some records, share out for you to read, everyone is really good. There are a lot of descriptions are excerpted from other articles, the following also gives a link, in fact, the official article has been the use of coroutines is very comprehensive, you can read the official article to learn, although it may not be very detailed, but the details are mentioned.

In the future, there may be some articles about advanced usage of coroutines, such as cold data Flow of coroutines, which has been used in our project, yes, I introduced 😁. In general, coroutine simple use is very simple, but want to use good, or need to work hard to study, but still can not escape the true fragrant law, we quickly learn to use it.

My other articles:

  • An Android MVVM componentized architecture framework
  • Unlock a new pose for managing EventBus registration — Custom annotations + Reflection
  • Explain, explain, explain, explain, explain

References and Extracts

Master Job&Deferred in Kotlin Coroutine

Bing-gan Huo, Understanding Kotlin Coroutines

Google developers – using coroutines in Android development | background introduction

Google developers – cancel and exception of coroutines | core concept is introduced