Public number: byte array, keen to share Android system source code parsing, Jetpack source code parsing, popular open source library source code parsing and other essential knowledge interview

Recently, I have been learning about kotlin coroutines, and the best learning materials are naturally the official learning documents. After reading them, I have the idea of translating the official documents. Before and after spent close to a month time, a total of nine articles, here also share out, hope to help readers. Limited by personal knowledge, some translation is not too smooth, also hope that readers can put forward their opinions

Coroutines official documentation: Coroutines – Guide

Coroutines-cn-guide coroutines-cn-guide

Coroutine official document Chinese translator: leavesC

[TOC]

Coroutines are always executed in some context represented by CoroutineContext defined in the Kotlin standard library

Coroutine contexts contain multiple subelements. The main elements are the coroutine Job (which we’ve seen before) and its Dispatche (described in this section)

1. Dispatchers and Threads

A Coroutine context contains a coroutine dispatcher (see CoroutineDispatcher) that determines the target carrier for execution of the coroutine, that is, which thread to run on, including one or more threads. The coroutine scheduler can restrict execution of a coroutine to a particular thread, dispatch it to a thread pool, or let it run indefinitely

All coroutine constructors, such as launch and async, take an optional parameter, CoroutineContext, which can be used to explicitly specify the scheduler to use for the coroutine and other context elements to be created

Try the following example:

import kotlinx.coroutines.*

fun main(a) = runBlocking<Unit> {
//sampleStart
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")}//sampleEnd    
}
Copy the code

The command output is as follows. The log output sequence may be different

Unconfined            : I'm working in thread main Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread main runBlocking : I'm working in thread main
Copy the code

When launch {… }, when used without arguments, inherits context and scheduler from the external coroutine scope. In this case, it inherits from the context of the runBlocking coroutine running in the main thread

Dispatchers.Unconfined is a special scheduler that appears to run in the main thread as well, but it’s actually a different mechanism, explained later

The default scheduler used when starting coroutines in GlobalScope is Dispatchers. Default and uses a shared background thread pool, so launch(Dispatchers. Default){… } and GlobalScope. Launch {… } is using the same scheduler

NewSingleThreadContext is used to create a new thread specifically for the coroutine to run. Dedicated threads are a very expensive resource. In a real application, it would have to be freed using the close function when it is no longer needed, or stored in a top-level variable for reuse throughout the application

| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |

Dispatchers.Unconfined Dispatchers start a coroutine in the caller thread, but it only runs to the first hanging point. After suspension, it resumes the coroutine in the thread, which is entirely determined by the suspended function called. The Unconfined scheduler is suitable for coroutines that consume neither CPU time nor update any shared data (such as UI) that is limited to a particular thread

The scheduler, on the other hand, inherits by default from the external coroutine scope. In particular, runBlocking starts a coroutine whose scheduler can only be the thread of the caller, so inheriting runBlocking results in a predictable FIFO of scheduled execution operations on that thread

import kotlinx.coroutines.*

fun main(a) = runBlocking<Unit> {
//sampleStart
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")}//sampleEnd    
}
Copy the code

Running result:

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main
Copy the code

So, from runBlocking{… } coroutines that inherit the context continue to execute in the main thread, and the dispatcher is an unconfined coroutine. By default, code following the delay function runs on the thread used by the delay function

The unconfined scheduler is an advanced mechanism that can help in extreme situations without the need to schedule a coroutine for later execution or unwanted side effects because certain operations must be performed immediately within the coroutine. Unrestricted schedulers should not be used in general code

3. Debugging Coroutines and Threads

Coroutines can be suspended on one thread and continue running on another. Even with a single-threaded scheduler, it can be difficult to know exactly what the coroutine is currently doing, where it is, and in what state. A common way to debug threads is to add a thread name to each log statement in the log file, which is generally supported by the logging framework. The thread name itself doesn’t provide much context information when using coroutines, so kotlinx.coroutines includes debugging tools to make it easier to debug coroutines

Open the JVM – Dkotlinx. Coroutines. After the debug configuration, run the following code:

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main(a) = runBlocking<Unit> {
//sampleStart
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
    //sampleEnd    
}
Copy the code

There are three coroutines. The main coroutine (#1) in runBlocking and the two coroutines that calculate the delay values A (#2) and b (#3). They are all executed in the context of runBlocking and are limited to the main thread. The output of this code is:

[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42
Copy the code

The log function prints the thread name in square brackets. You can see that the coroutines are running on the main thread, followed by the identifier of the currently executing coroutine. When debug mode is turned on, this identifier is assigned consecutively to all coroutines that are created

Debug mode is also turned on when running the JVM with the -ea option, and you can read more about the debug tool in the DEBUG_PROPERTY_NAME property document

4. Jumping between threads

Open the JVM – Dkotlinx. Coroutines. After the debug configuration, run the following code:

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main(a) {
//sampleStart
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")}}}//sampleEnd    
}
Copy the code

Several new techniques are demonstrated here. One is to use runBlocking to display the specified context, and the other is to use the withContext function to change the context of a coroutine while still remaining in the other coroutine, as you can see in the output below:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1
Copy the code

Note that this example also uses the use function from the Kotlin standard library to release the threads created by newSingleThreadContext when they are no longer needed

Job in the context

A Job in a coroutine is part of its context and can be obtained from that context through the coroutineContext[Job] expression

import kotlinx.coroutines.*

fun main(a) = runBlocking<Unit> {
//sampleStart
    println("My job is ${coroutineContext[Job]}")
//sampleEnd    
}
Copy the code

In debug mode, the output is similar to:

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
Copy the code

Note that the isActive attribute of CoroutineScope is just coroutineContext[Job]? A shorthand for.isactive == true

public val CoroutineScope.isActive: Boolean
    get() = coroutineContext[Job]? .isActive ? :true
Copy the code

Children of a coroutine

When a collaborators in another collaborators cheng cheng coroutines scope starts, it will be sent to you by CoroutineScope. CoroutineContext inherit its context, new start coroutines Job would become the father of coroutines Job Job. When a parent coroutine is cancelled, all of its child coroutines are also cancelled recursively

However, when starting a coroutine using GlobalScope, the coroutine’s Job has no parent. As a result, it is not limited by the scope in which it starts and the scope in which it operates independently

import kotlinx.coroutines.*

fun main(a) = runBlocking<Unit> {
//sampleStart
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        // it spawns two other jobs, one with GlobalScope
        GlobalScope.launch {
            println("job1: I run in GlobalScope and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")}// and the other inherits the parent context
        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel() // cancel processing of the request
    delay(1000) // delay a second to see what happens
    println("main: Who has survived request cancellation?")
//sampleEnd
}
Copy the code

The result of this operation is:

job1: I run in GlobalScope and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?
Copy the code

Parental responsibilities

A parent coroutine always waits for all of its child coroutines to complete. The parent coroutine does not have to explicitly keep track of all the subcoroutines it starts, nor does it have to use job. join to wait at the end for the subcoroutine to complete

import kotlinx.coroutines.*

fun main(a) = runBlocking<Unit> {
//sampleStart
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        repeat(3) { i -> // launch a few children jobs
            launch  {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // wait for completion of the request, including all its children
    println("Now processing of the request is complete")
//sampleEnd
}
Copy the code

Running result:

request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete
Copy the code

Naming Coroutines for Debugging

The ID automatically assigned by the coroutine is useful when the coroutine often needs to be logged, and you only need to associate the log records from the same coroutine. However, when a coroutine is bound to the processing of a particular request or the execution of a particular background task, it is best to explicitly name it for debugging purposes. The CoroutineName context element serves the same purpose as the thread name, which is contained in the name of the thread that executes this coroutine when debugging mode is enabled

The following code demonstrates this concept:

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main(a) = runBlocking(CoroutineName("main")) {
//sampleStart
    log("Started main coroutine")
    // run two background value computations
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        252
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        6
    }
    log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
//sampleEnd    
}
Copy the code

Open – Dkotlinx. Coroutines. Debug output after the JVM configuration is similar to:

[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42
Copy the code

9. Combining context Elements

Sometimes we need to define more than one element for a coroutine context. We can use the + operator. For example, we can start a coroutine with both an explicitly specified scheduler and an explicitly specified name

import kotlinx.coroutines.*

fun main(a) = runBlocking<Unit> {
//sampleStart
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")}//sampleEnd    
}
Copy the code

Open – Dkotlinx. Coroutines. Debug output after the JVM configuration is:

I'm working in thread DefaultDispatcher-worker-1 @test#2
Copy the code

Coroutine scope (Coroutine scope)

Let’s put together the knowledge about scopes, subelements, and jobs. Suppose our application has an object that has a life cycle, but that object is not a coroutine. For example, we are writing an Android application and launching various coroutines in an Android Activity to perform asynchronous operations to get and update data, specify animations, and so on. When Actovoty is destroyed, all coroutines must be cancelled to avoid memory leaks. Of course, we can manually manipulate the context and Job to bind the Activity and coroutine lifecycles. However, kotlinx.coroutines provides an abstract encapsulation: CoroutineScope. You should already be familiar with coroutine scope, since all coroutine constructors are declared as its extension functions

We manage the coroutine lifecycle by creating instances of the coroutine scope associated with the Activity lifecycle. Instances of CoroutineScope can be built using the factory functions of CoroutineScope() or MainScope(). The former creates the generic scope, while the latter creates the scope of the UI application and uses dispatchers.main as the default scheduler

class Activity {
    private val mainScope = MainScope()
    
    fun destroy(a) {
        mainScope.cancel()
    }
    // to be continued ...
}
Copy the code

Alternatively, we can implement the CoroutineScope interface in the Activity class. The best way to do this is to use delegates for the default factory functions. We can also combine the required scheduler (dispatchers.default in this case) with the scope:

    class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
    // to be continued ...
Copy the code

We can now launch coroutines within the Activity without specifying their context explicitly. To demonstrate, we started ten coroutines with different delays:

    // class Activity continues
    fun doSomething(a) {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")}}}}// class Activity ends
Copy the code

In the main function, we create the Activity object, call the test doSomething function, and destroy the Activity after 500 milliseconds. This will cancel all coroutines launched from doSomething. We can see this because after destroying the Activity object, no more messages are printed, even if we wait a little longer

import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

    fun destroy(a) {
        cancel() // Extension on CoroutineScope
    }
    // to be continued ...

    // class Activity continues
    fun doSomething(a) {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")}}}}// class Activity ends

fun main(a) = runBlocking<Unit> {
//sampleStart
    val activity = Activity()
    activity.doSomething() // run test function
    println("Launched coroutines")
    delay(500L) // delay for half a second
    println("Destroying activity!")
    activity.destroy() // cancels all coroutines
    delay(1000) // visually confirm that they don't work
//sampleEnd    
}
Copy the code

Output results:

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!
Copy the code

As you can see, only the first two coroutines print a message; the others are cancelled by job.cancel() in actiw.destroy ()

11. Thread-local data

Sometimes it is useful to pass some thread-local data to or between coroutines. However, since the coroutine is not bound to any particular thread, this can result in template code if done manually

With ThreadLocal, the extension function asContextElement can be used to solve this problem. It creates an additional context element that holds the value given by ThreadLocal and restores it each time the coroutine switches its context

It is easy to prove in practice:

import kotlinx.coroutines.*

valthreadLocal = ThreadLocal<String? > ()// declare thread-local variable

fun main(a) = runBlocking<Unit> {
//sampleStart
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
//sampleEnd    
}
Copy the code

In this case, we use Dispatchers.Default to start a new coroutine in the background thread pool because it can switch back and forth between the different threads in the thread pool, But it still has us using threadLocal. AsContextElement (value = “launch”) of the specified thread local variable value, no matter on which thread coroutines. Therefore, the output (with debug mode enabled) is:

Pre-main, current thread: Thread[main @coroutine#1.5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2.5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2.5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1.5,main], thread local value: 'main'
Copy the code

It’s easy to forget to set the appropriate context element. If more than one thread is running the coroutine, thread-local variables accessed from the coroutine may have unexpected values. In order to avoid this situation, it is recommended to use the ensurePresent method, which can fail quickly if used improperly

ThreadLocal has first-class support and can be used with any base kotlinx.Coroutines. However, it has a key limitation: when a thread-local variable changes, the new value is not transmitted to the coroutine caller (because the context element cannot track all thread-local object references). And the updated values are lost on the next hang. Update the local value of the thread in the coroutine withContext. For more information, see asContextElement

Alternatively, the value can be stored in a mutable class counter (var I: Int), which in turn is stored in a thread-local variable, but in that case you are fully responsible for synchronizing potential concurrent changes to the variables in that counter

For more advanced uses, such as integration with logging MDC, Transactional Contexts, or other libraries that pass data internally using thread-local variables, see the documentation that implements the ThreadContextElement interface