[kotlin coroutine] Wanzi coroutine, a complete kotlin coroutine advanced

Kotlin Coroutine Advanced

  • Introduction to coroutines
  • 1. Basic use of coroutines
    • 1.1, runBlocking startup
    • 1.2, GlobalScope .launch startup
    • 1.3, GlobalScope.async startup
    • 1.4, description of three startup methods
  • Second, Coroutine source code analysis
    • 2.1, CoroutineContext
    • 2.2, Job source code
    • 2.3, Job’s Commonly used functions
    • 2.4, SupervisorJob
  • Third, suspend keyword
    • 3.1, CoroutineDispatcher scheduler
    • 3.2, CoroutineStart coroutine startup mode
    • 3.3, CoroutineScope – Coroutine Scope
    • 3.4, Classification and Behavior Rules
  • Fourth, the use and cancellation of coroutines and exceptions in Android
    • 4.1, using SupervisorJob
    • 4.2, using supervisorScope

Introduction to coroutines

Kotlin provide a new way to handle concurrency, which can be used on the Android platform to simplify asynchronously executed code. Coroutines have been introduced since Kotlin 1.3, but this concept has existed since the dawn of the programming world. The earliest programming language using coroutines can be traced back to 1967 Years of the Simula language.
In the past few years, the concept of coroutines has developed rapidly and has been adopted by many mainstream programming languages, such as Javascript, C#, Python, Ruby, Go and more. Kotlin‘s coroutines are based on established concepts from other languages.

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

  • Processing time-consuming tasks (Long running tasks), such tasks often block the main thread;
  • Ensure that the main thread is safe (Main- safety) , which ensures that any suspend functions are safely called from the main thread.

In essence, a coroutine is a lightweight thread.

1. Basic use of coroutines

Before using the coroutine, we need to introduce the Coroutine package

// 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"

There are many ways to create coroutines. Here we will not extend the advanced usage of coroutines (hot data channel Channel, cold data flow Flow.…), create a coroutine Here are three commonly used methods:

1.1, runBlocking start

runBlocking {
    println("runBlocking starts a coroutine")
}

runBlocking Starting a coroutine will block the calling thread until the code inside is executed, and the return value is genericT.

1.2, GlobalScope.launch start

GlobalScope.launch {
    println("launch starts a coroutine")
}

launch Starting a coroutine will not block the calling thread, it must be called in the coroutine scope (CoroutineScope), and the return value is a Job

1.3, GlobalScope.async start

GlobalScope. async {
    the pricentln("async starts a coroutine")
}

async Starting a coroutine is actually the same as launch, the difference is that the return parameter of async is: Deferred Deferred : Job, which implements a Deferred interface, but Deferred inherits job. The difference between Deferred and job is that Deferred defines The await function needs to be used in conjunction with the await() suspending function.

1.4, description of three startup methods

  • runBlocking{} – primarily for testing

    This method is designed to allow libraries written in the suspend style Can be used in regular blocking code, often in main methods and tests.

  • GlobalScope.launch/async{} – not recommended

    Because the coroutine launched in this way exists The situation where the component has been destroyed but the coroutine still exists may lead to resource exhaustion in extreme cases, so it is not recommended to start this way, especially in scenarios where components need to be frequently created and destroyed on the client side.

2. Coroutine source code analysis

Here we use the source code of CoroutineScope.launch{} as an example to learn more about Coroutine:

The above is launchfunction, which appears as an extension function of CoroutineScope, the function parameters are: coroutineContext, coroutineStart code>, coroutine body, the return value is coroutine instance Job, where CoroutineContext includes Job, CoroutineDispatcher, CoroutineName. Below we will introduce these contents one by one: CoroutineContext, Job, CoroutineDispatcher, CoroutineStart, CoroutineScope.

2.1, CoroutineContext

CoroutineContext: Coroutine context

  1. Thread behavior, life cycle, exception and debugging
  2. Contains some user-defined data collections, which are closely related to coroutines
  3. It is an indexed Element instance collection, a data structure between set and map. Each element has a unique Key in this collection
  • Job: Control the life cycle of coroutines

  • CoroutineDispatcher: Distribute tasks to appropriate threads

  • CoroutineName: The name of the coroutine, which is useful for debugging

  • CoroutineExceptionHandler: Handling uncaught exceptions

CoroutineContext has two very important elements — Job and Dispatcher, Job is the current Coroutine instance and Dispatcher determines the current Coroutine execution thread, You can also add CoroutineName for debugging, add CoroutineExceptionHandler for catching exceptions, and they all implement the Element interface.

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

Output:

CoroutineContext source code:

Through the source code, I can see that CoroutineContext defines four core operations:

  • Operator get
    can get this Element through key. Since this is a get operator, it can be accessed in the form of square brackets context[key] like accessing elements in a map.
  • The operator plus
    is similar to the Set.plus extension function, returning a new context object, The new object contains all the Element in the two. If there is a duplicate (the same Key), then use the Element on the right side of the + number code> replaces the one on the left. The + operator can easily be used to combine contexts, but there is one very important thing to be careful about - pay attention to the order in which they combine, because this + operator is asymmetrical.
  • fun fold(initial: R, operation: (R, Element) -> R): R
    and Collection.fold extension Functions are similar, providing the ability to traverse all Element in the current context.
  • fun minusKey(key: Key): CoroutineContext
    Returns a context that contains elements in that context, but does not contain elements with the specified key element.

2.2, Job source code

Job is used to process coroutines

For each coroutine created (via launch or async), it will return a Job instance, which is the unique identifier of the coroutine and is responsible for Manage the lifecycle of coroutines
The CoroutineScope.launch function returns a Job object, representing an asynchronous task. Job has a lifecycle and can be canceled. Job can also have a hierarchical relationship, a Job can contain multiple child Job, when the parent Job is canceled, All child Job will also be automatically canceled; when the child Job is canceled or an exception occurs, the parent Job will also be canceled.
In addition to creating a Job object through CoroutineScope.launch, you can also create the object through the Job() factory method. By default, failure of a child Job will cause the parent Job to be canceled, this default behavior can be modified by SupervisorJob.
A parent Job with multiple child Job will wait for all child Job to complete (or cancel) before executing itself

Status of the Job Lifecycle

A task can have a range of states: newly created (New), active (Active), completing (Completing), Completed, Canceling (Cancelling), and Canceled (Cancelled). While we can't access these states directly, we can access the properties of Job: isActive, isCancelled, and isCompleted.
If the coroutine is in the active state, the coroutine running error or calling job.cancel() will put the current task into the canceling (Cancelling) state (isActive = false, isCancelled = true). When all child coroutines are completed, the coroutine will enter the canceled (Cancelled) state, at this time isCompleted = true.

2.3, common functions of Job

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

  • fun start(): Boolean
    Call this function to start this Coroutine, if the current Coroutine Calling this function returns true if it has not been executed yet. If the current Coroutine has been executed or has been executed, calling this function returns false

  • fun cancel(cause: CancellationException? = null)
    Cancels this job with an optional cancellation reason. The reason can be used to specify an error message or provide additional details about the reason for the cancellation, for debugging.

  • fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
    Through this function you can set a completion for Job Notification, this notification function will be executed synchronously when the execution of Job is completed. The notification object type of the callback is: typealias CompletionHandler = (cause: Throwable?) -> Unit. The CompletionHandler parameter represents how the Job is executed of. cause has the following three situations:

    1. If the Job is executed normally, the cause parameter is null
    1. If the Job was canceled gracefully, the cause parameter is a CancellationException object. This situation should not be treated as an error, it is a case of normal cancellation of the task. So there is generally no need to record this situation in the error log.
    2. Other cases indicate that Job failed to execute.

The return value of this function is a DisposableHandle object. If you no longer need to monitor the completion of Job, you can call DisposableHandle.dispose function to cancel listening. If the Job has been executed, there is no need to call the dispose function, and the monitoring will be canceled automatically.

  • suspend fun join()

join function is different from the previous three functions, this is a suspend function. So it can only be called within a Coroutine.

This function will suspend the current Coroutine until the execution of the Coroutine is completed. So the join function is generally used in another Coroutine to wait for the completion of job to continue execution. When the execution of Job is completed, the job.join function resumes. At this time, the task of job is already in the completed state, and calling job The Coroutine of the .join continues to be in the activie state.

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

The suspension of this function can be canceled, and always check whether the Job of the Coroutine called is cancelled. If the Job that called the Coroutine is canceled or completed when this suspend function is called or suspended, this function will throw a CancellationException .

2.4, Supervisor Job

SupervisorJob is a top-level function defined as follows:

This function creates a supervisor job in the active state. as beforeAs mentioned above, Job has a parent-child relationship. If the child Job fails, the parent Job will fail automatically. This default behavior may not be ours Expected. For example, there are two child Job in Activity to obtain the comment content and author information of an article respectively. If one of them fails, we don't want the parent Job to be canceled automatically, which will cause the other child Job to be canceled as well. And SupervisorJob is such a special Job, the child Job does not affect each other, a child Job fails , does not affect the execution of other child Job. SupervisorJob(parent:Job?) has a parent parameter, if this parameter is specified, the returned Job is the parameter parent child Job. If the Parent Job fails or is cancelled, the Supervisor Job will also be cancelled. When a Supervisor Job is canceled, all child Job of Supervisor Job will also be canceled.

MainScope() is implemented using SupervisorJob and a Main Dispatcher:

But SupervisorJob is easy to be misunderstood. It is related to coroutine exception handling and sub-coroutines.The >Job type and domain are confusing. For specific exception handling, please refer to this Google article: Cancellation and Exceptions in Coroutines | Exception Handling Details

Three, suspend keyword

This suspend keyword, since it does not really implement suspension, what is its function?

It is actually a reminder.

The creator of the function reminds the user of the function: I am a time-consuming function, and I am put in the background by my creator in a suspended manner, so please call me in the coroutine.

The suspended operation—that is, thread cutting, depends on the actual code in the suspended function, not on this keyword.

So this keyword, is just a reminder.

3.1, CoroutineDispatcher scheduler

  • Dispatchers.Default

    The default scheduler is suitable for processing background calculations and is a CPU-intensive task scheduler. If no dispatcher is specified when creating a Coroutine, this is generally used as the default value by default. The Default dispatcher uses a shared pool of background threads to run its tasks. Note that it shares the thread pool with IO, but only limits the maximum number of concurrency.

  • Dispatchers.IO

    As the name implies, this is used to perform blocking IO operations, and shares a shared thread with Default Pool to perform tasks inside. Depending on the number of tasks running at the same time, additional threads will be created when needed, and unneeded threads will be released when the tasks are completed.

  • Dispatchers.Unconfined

    Since Dispatchers.Unconfined does not define a thread pool, it starts the thread by default during execution. The first suspension point is encountered, after which theThe thread calling resume determines the thread that resumes the coroutine.

  • Dispatchers.Main

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

Because the child Coroutine will inherit the context of the parent Coroutine, for the convenience of use, we generally set a Dispatcher on the parent Coroutine, and then all child Coroutines will automatically use this Dispatcher.

3.2, CoroutineStart coroutine startup mode

  • CoroutineStart.DEFAULT

    Start scheduling immediately after the coroutine is created, if the coroutine is canceled before scheduling, it will directly enter the cancellation response Although the state is scheduled immediately, it may also be canceled before execution

  • CoroutineStart.ATOMIC

    Immediately after the coroutine is created Start scheduling, and the coroutine execution will not respond to cancellation before reaching the first suspension point
    Although it is immediate scheduling, it combines the two steps of scheduling and execution into one, just like its name, it guarantees scheduling And execution is an atomic operation, so the coroutine will also be executed

  • CoroutineStart.LAZY

    As long as the coroutine is needed , including actively calling the start, join or await functions of the coroutine, the scheduling will start. If it is canceled before scheduling, the coroutine will directly enter the abnormal end state

  • CoroutineStart.UNDISPATCHED

    Immediately after the coroutine is created, it is executed in the current function call stack until it encounters the first real suspension point
    is executed immediately, so The coroutine will definitely execute

The design of these startup modes is mainly to deal with some special scenarios. business developmentIn practice, it is usually enough to use the two startup modes DEFAULT and LAZY

3.3, CoroutineScope – coroutine scope

Defining a coroutine must specify its CoroutineScope . CoroutineScope can track the coroutine even if the coroutine is suspended. Unlike the dispatcher (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 started without using CoroutineScope. CoroutineScope can be thought of as a lightweight version of ExecutorService with superpowers. CoroutineScope keeps track of all coroutines, and it can also cancel all coroutines started by it. This is very useful in Android development, for example, it can stop the execution of the coroutine when the user leaves the interface.

Coroutine is a lightweight thread, which does not mean that it does not consume system resources. When the asynchronous operation is time-consuming, or when an error occurs in the asynchronous operation, the Coroutine needs to be canceled to release system resources. In the Android environment, usually the Coroutine started by each interface (Activity, Fragment, etc.) Makes sense, if the user exits the interface while waiting for the Coroutine to execute, it may not be necessary to continue executing the Coroutine. Additionally Coroutine also needs to be executed in a proper context, otherwise errors will occur, such as accessing View in a non-UI thread. Therefore, when designing Coroutine, it is required to be executed within a scope (Scope), so that when this Scope is cancelled, all the child Coroutine is also automatically canceled. So to use Coroutine you must first create a corresponding CoroutineScope.

CoroutineScope interface

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope just defines a new Coroutine execution Scope. Each coroutine builder is an extension function of CoroutineScope and automatically inherits the coroutineContext of the current Scope.

3.4, classification and rules of conduct

The official framework also provides a scope in the process of implementing composite coroutines, which is mainly used to clarify the parent-child relationship between writing, as well as the propagation behavior for cancellation or exception handling. This scope includes the following three types:

  • Top-level scope
    The scope of a coroutine without a parent coroutine is the top-level scope.

  • Synergy Scope
    A new coroutine is started in a coroutine, and the new coroutine is a sub-coroutine of the existing coroutine. In this case, the scope of the sub-coroutine is the collaboration scope by default. At this time, the uncaught exception thrown by the child coroutine will be passed to the parent coroutine for processing, and the parent coroutine will also be canceled at the same time.

    coroutineScope The internal exceptions will be propagated upwards, and the uncaught exceptions of the sub-coroutines will be passed up to the parent coroutines. Any abnormal exit of a sub-coroutine will lead to the overall exit

  • Master-slave role Domain
    is consistent with the synergy scope in the parent-child relationship of the coroutine, the difference is that when the coroutine under this scope has an uncaught exception, the exception will not be passed up to the parent coroutine .

    supervisorScope belongs to the master-slave scope and will inherit the context of the parent coroutine. Its characteristic is that the exception of the child coroutine will not affect the parent coroutine

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

  • If the parent coroutine is canceled, all child coroutines will be cancelled. Since there is a parent-child coroutine relationship in both the coroutine scope and the master-detail scope, this rule applies to both.
  • The parent coroutine needs to wait for the child coroutine to finish executing before finally entering the completion state, regardless of whether the coroutine body of the parent coroutine itself has been executed.
  • The child coroutine will inherit the elements in the coroutine context of the parent coroutine. If it has members with the same key, it will overwrite the corresponding key, the overriding effect is only valid within its own range.

Fourth, the use and cancellation of coroutines and exceptions in Android

If an ordinary coroutine generates an unhandled exception, it will propagate the exception to its parent coroutine, and then the parent coroutine will cancel all child coroutines, cancel itself, and continue to pass the exception upwards. Let’s take an official picture to illustrate this process:

Sometimes this situation is not what we want. We hope that when a coroutine generates an exception, it will not affect the execution of other coroutines. We have also mentioned some solutions above, and we will introduce them below Practice it.

4.1, using SupervisorJob

 // Use MainScope() of the official library to obtain a coroutine scope for creating coroutines
    private val mScope = MainScope();

    fun onClickCoroutine(view: View) {

        mScope.launch(Dispatchers.Default) {
            println("I am the first coroutine")
        }

        mScope.launch(Dispatchers.Default) {
            println("I am the second coroutine")
            throw RuntimeException("RuntimeException is an exception")
        }

        mScope.launch(Dispatchers.Default) {
            println("I am the third coroutine")
        }
    }

Code execution result:

MainScope() mentioned before, its implementation is to use SupervisorJob. The execution result is that the second coroutine throws an exception, the third coroutine executes normally, But the program crashed, because we didn’t handle this exception, let’s improve the code

Exception handling:

fun onClickCoroutine(view: View) {

    mScope.launch(Dispatchers.Default) {
        println("I am the first coroutine")
    }

    mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
        println(
            "CoroutineExceptionHandler: $throwable"
        )
    }) {
        println("I am the second coroutine")
        throw RuntimeException("RuntimeException is an exception")
    }

    mScope.launch(Dispatchers.Default) {
        println("I am the third coroutine")
    }
}

Print result:

The program does not crash, and the exception handling print is also output, which achieves the effect we want. But pay attention to one thing, the parent of these sub-coroutines is SupervisorJob, but if they have sub-coroutines, the parent of their sub-coroutines is not SupervisorJob, so when they generate When it is abnormal, it is not the effect we demonstrated. We use an official diagram to explain this relationship:

As shown in the figure, when a new coroutine is created, a new Job instance will be generated to replace SupervisorJob.

4.2, using supervisorScope

This scope is also mentioned above, using supervisorScope can also achieve the effect we want, the above code:

fun onClickCoroutine(view: View) {

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

    coroutineScope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
        println(
            "CoroutineExceptionHandler: $throwable"
        )
    }) {
        supervisorScope {
            launch {println("I am the first coroutine")
            }
            launch {
                println("I am the second coroutine")
                throw RuntimeException("RuntimeException is an exception")
            }
            launch {
                println("I am the third coroutine")
            }
        }
    }
}

Run results

It can be seen that the effect we want has been achieved, but if we replace supervisorScope with coroutineScope, the result is not like this. In the end, I still use the official picture to show it:

This is the end of the article. This article refers to Quyunshuo. If there is any infringement, please contact to delete it.

Leave a Reply

Your email address will not be published. Required fields are marked *