Kotlin syntax advanced - coroutine (1) coroutine basics

1. Understanding of kotlin coroutines

Coroutines are not a new concept, but a very old concept. Many languages ​​support coroutines. It is recommended to go to the browser to understand the history and basic concepts of coroutines. Here we will only talk about coroutines in kotlin. effect.

From the perspective of code implementation: the bottom layer of kotlin coroutine is implemented using threads. It is a fully encapsulated thread framework for developers to use. A coroutine in kotlin can be understood as an execution task running on a thread, and the task can be switched between different threads. A thread can run multiple coroutines at the same time. The conceptual diagram is as follows: A thread can run multiple coroutines at the same time
. (Perform tasks), if necessary, the coroutine on thread 1 can be switched to thread 2 to run. Reflected in our Android development, our network requests and other time-consuming tasks such as database operations run on the IO thread. When we obtain the data, the page update operation runs on the main thread.
Insert image description here

From a developer's perspective: Kotlin coroutines can write asynchronous execution code in a synchronous manner and solve the nesting hell of thread switching callbacks . There is no need to block the thread when the coroutine is suspended, and it is almost free.
This reflects our Android development. In the past, after we got the data in the IO thread, we wanted to return to the main thread to update the UI. We usually used Handler or CallBack interface callbacks. The result of this is that when we continuously perform the above operations, it will be very long. It is easy to fall into nesting hell, which seriously affects the readability and beauty of the code.

2. How to create a coroutine

We want to use coroutines in Android Studio. In addition to introducing kotlin support, we also need to introduce two coroutine support libraries.

// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"

How to create a coroutine

2.1.) runBlocking: This is a top-level function that will start a new coroutine and block the thread that calls it until the code inside is executed. The return value is a generic T.
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
2.2) CoroutineScope.launch: Launch a coroutine through the extension method launch of a coroutine scope. It will not block the thread that calls it. The return value is Job.
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
2.3) CoroutineScope.async: Start a coroutine through the async extension method of a coroutine scope. It will not block the thread that calls it, and the return value is Deferred.
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

We generally use two methods 2) and 3) to start a coroutine. Method 1) will block the thread, so we basically will not use the runBlocking function in development, but it can be used for code debugging.
Let me talk about the difference between async and launch in advance. I will talk about it later with examples:
the return value is different: the last line of code expression in the async function body will return the result as the result, which is the generic T in Deferred. We can use other The coroutine function obtains this execution result, but launch does not have such a return value.

Sample code: GlobalScope is a coroutine scope already provided in the coroutine library

runBlocking {
    
    
    Log.e("协程","我们使用runBlocking启动了一个协程")
}
GlobalScope.launch {
    
    
    Log.e("协程","我们使用launch启动了一个协程")
}
GlobalScope.async {
    
    
    Log.e("协程","我们使用async启动了一个协程")
}

3. Coroutine scope and coroutine context

We mentioned earlier that among the three ways to create a coroutine, launch and async both require the coroutine scope to be used to open a coroutine.

3.1 What is coroutine scope?

Coroutine scope CoroutineScope is the running scope of the coroutine.
As follows: CoroutineScope is an interface with only one interface attribute, CoroutineContext, so CoroutineScope is actually an encapsulation of the CoroutineContext coroutine context.

public interface CoroutineScope {
    
    
    public val coroutineContext: CoroutineContext
}

3.2 What is coroutine context?

CoroutineContext represents the context of the coroutine . Before we understand the coroutine context, we first understand the Context context. Android developers should be familiar with this attribute. Our Application, Activity, etc. are all defined as a Context. So how do we understand this Context? For example, when we take reading comprehension tests in school, we need to understand the meaning of a passage and what we should do. We need to substitute the original article, and then look at the front and see what is going on behind to understand. Substituting into the code, we have run this code. We also need to understand how to execute this code and what information is needed to execute it. This information is stored in the Context. We can understand the Context as a container for storage. Configuration information required for code execution. So CoroutineContext is the configuration information we need to start a coroutine.

Let's take a look at the source code implementation of CoroutineContext. It is an interface. There is an internal interface Element inside the interface (this interface inherits from CoroutineContext. The Element interface has a key, which is similar to a key-value pair. We can use the key to judge the Element). We see that
we You can perform operations on CoroutineContext such as get (get a CoroutineContext based on the key), plus (the return value of a plus operator overload is CoroutineContext, which means CoroutineContext+CoroutineContext =CoroutineContext), minusKey (remove the CoroutineContext corresponding to the key), and watch It is really like a set operation. In fact, CoroutineContext is similar to a set operation, so we can guess that CoroutineContext has many subclasses. These subclasses can be added and spliced, and minusKeys can be subtracted to form a variety of CoroutineContexts.

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 =
        if (context === EmptyCoroutineContext) this else 
            context.fold(this) {
    
     acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
    
    
    
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
    
    
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

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

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
    
    

        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? 
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}
3.3 Coroutine scope and sub-coroutines

We can start another coroutine in a coroutine, and the coroutine opened in this way is a sub-coroutine.
There are two points to note:
1.) The coroutine scope of the child coroutine will inherit the coroutine context in the parent coroutine scope.
2.) If the parent coroutine is canceled, all child coroutines will also be canceled. Cancel

We will verify these two attention points in turn below.

3.4 Those subclasses of CoroutineContext

CoroutineContext has many subclasses, each of which has different functions, and together constitute the CoroutineContext in the coroutine scope.
Let’s first print the coroutineContext of a coroutine scope and see what is printed.

GlobalScope.launch{
    
    
    Log.e("协程的coroutineContext",this.coroutineContext.toString())
}
打印结果:
[StandaloneCoroutine{
    
    Active}@5a7652d, Dispatchers.Default]

We understand these subclasses one by one and understand their impact and role on coroutines

3.4.1)CoroutineDispatcher coroutine scheduler

We know that coroutines run in threads and can be switched to other threads. CoroutineDispatcher determines which thread or threads the relevant coroutines are executed on, so CoroutineDispatcher can also be called the thread scheduler of coroutines.

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    
    

    @ExperimentalStdlibApi
    public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
        ContinuationInterceptor,
        {
    
     it as? CoroutineDispatcher })
}

Kotlin provides us with four schedulers

public actual object Dispatchers {
    
    
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

Default: The default scheduler, a CPU-intensive task scheduler, usually handles some simple computing tasks, or tasks with short execution time. For example, data calculation
IO: IO scheduler, IO-intensive task scheduler, suitable for performing IO-related operations. For example: network requests, database operations, file operations, etc.
Main: UI scheduler, only meaningful on the UI programming platform, used to update the UI, such as the main thread in Android. Unconfined:
unrestricted scheduler, no scheduler, currently Coroutines can run on any thread.

From the above print, we can know that GlobalScope's coroutine scheduler is Dispatchers.Default, so how do we change it? When we looked at the launch and async methods earlier, we saw that their first parameters were context: CoroutineContext. Yes, we can pass in the context we need from here, and it will overwrite the context in the coroutine scope.
As follows: We hope to start a coroutine to run on the IO thread

GlobalScope.launch(Dispatchers.IO){
    
    
    Log.e("协程的coroutineContext",this.coroutineContext.toString())
}
打印结果:
[StandaloneCoroutine{
    
    Active}@db90566, Dispatchers.IO]

So what if we want to change the thread while the coroutine is running? Most commonly, network requests are in the IO thread, and page updates are in the main thread. Kotlin provides us with a top-level function withContext to change the context of the coroutine and execute a piece of code.

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 

Sample code:

GlobalScope.launch(Dispatchers.Main) {
    
    
    val result = withContext(Dispatchers.IO) {
    
    
        //网络请求
        "返回结果"
    }
    mBtn.text = result
}
3.4.2)CoroutineName coroutine name

Coroutine name: As the name suggests, it is to give the coroutine a name. There is nothing to say about this, it is used to better distinguish the coroutine.

public data class CoroutineName(

    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    
    

    public companion object Key : CoroutineContext.Key<CoroutineName>

    override fun toString(): String = "CoroutineName($name)"
}

Sample code:

GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) {
    
    
    Log.e("协程的coroutineContext",this.coroutineContext.toString())
}
打印结果:
[CoroutineName(主协程), StandaloneCoroutine{
    
    Active}@288ff9, Dispatchers.Main]

The first point to note when we use the coroutine name to verify the sub-coroutine: the coroutine scope of the sub-coroutine will inherit the coroutine context in the parent coroutine scope. We start a sub-coroutine in the above code
:

GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) {
    
    
    Log.e("协程的coroutineContext" , this.coroutineContext.toString())
    launch {
    
    
        Log.e("协程的coroutineContext2" , this.coroutineContext.toString())
    }
}

The printing result is as follows: It can be seen that the child coroutine actually printed the same CoroutineName as the parent coroutine, indicating that the coroutine context of the parent coroutine has been propagated to the child coroutine.

协程的coroutineContext: [CoroutineName(主协程), StandaloneCoroutine{
    
    Active}@288ff9, Dispatchers.Main]
协程的coroutineContext2: [CoroutineName(主协程), StandaloneCoroutine{
    
    Active}@b95b3e, Dispatchers.Main]

Let’s change the code a little bit:

GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) {
    
    
    Log.e("协程的coroutineContext" , this.coroutineContext.toString())
    launch(CoroutineName("子协程")) {
    
    
        Log.e("协程的coroutineContext2" , this.coroutineContext.toString())
    }
}

The print result is as follows: The coroutine context set by the child coroutine overwrites the context inherited from the parent coroutine.

协程的coroutineContext: [CoroutineName(主协程), StandaloneCoroutine{
    
    Active}@288ff9, Dispatchers.Main]
协程的coroutineContext2: [CoroutineName(子协程), StandaloneCoroutine{
    
    Active}@8aced9f, Dispatchers.Main]
3.4.3) Life cycle of Job and coroutine

When we looked at the two extension functions launch and async earlier, we can see that the return result of launch is a Job, and the return result of async is a Deferred. Deferred is actually a subclass of Job.

public interface Job : CoroutineContext.Element 
public interface Deferred<out T> : Job 

So what is a job?
After the coroutine is started, we can get a Job object. Through the Job object, we can detect the life cycle status of the coroutine and operate the coroutine (such as canceling the coroutine) . We can roughly understand Job as the coroutine itself.
The life cycle of the coroutine: After the coroutine is created, it is in the New (new) state. After the coroutine is started (the start() method is called), it is in the Active (active) state. After the coroutine and all sub-coroutines complete their tasks, it is in the Completed state. (Complete) state. After the coroutine is canceled (cancel() method is called), it is in the Cancelled state.
We can use the fields under the job to check the status of the coroutine:
isActive is used to determine whether the coroutine is active.
isCancelled is used
isCompleted is used to determine whether the coroutine has ended
. In addition to obtaining the coroutine status, there are many functions that can be used to manipulate the coroutine, such as:
cancel() to cancel the coroutine.
start() starts the coroutine.
await() waits for the completion of coroutine execution

Let’s verify the life cycle of the coroutine:

GlobalScope.launch {
    
    
    val job = launch(CoroutineName("子协程")) {
    
    

    }
    Log.e("子协程的状态","${
      
      job.isActive} ${
      
      job.isCancelled} ${
      
      job.isCompleted}")
    delay(1000)
    Log.e("子协程的状态2","${
      
      job.isActive} ${
      
      job.isCancelled} ${
      
      job.isCompleted}")
}
打印结果:
子协程的状态: true false false
子协程的状态2: false false true
GlobalScope.launch {
    
    
    val job = launch(CoroutineName("子协程")) {
    
    
        delay(5000)
    }
    Log.e("子协程的状态","${
      
      job.isActive} ${
      
      job.isCancelled} ${
      
      job.isCompleted}")
    job.cancel()
    Log.e("取消后子协程的状态","${
      
      job.isActive} ${
      
      job.isCancelled} ${
      
      job.isCompleted}")
}
打印结果:
子协程的状态: true false false
取消后子协程的状态: false true false

Let’s use the life cycle of the coroutine to verify the second note of the sub-coroutine: if the parent coroutine is canceled, all sub-coroutines will also be cancelled.

var childJob : Job? = null
val parentJob = GlobalScope.launch {
    
    
    childJob = launch(CoroutineName("子协程")) {
    
    
        delay(5000)
    }
}
Log.e("父协程的状态" , "${
      
      parentJob.isActive} ${
      
      parentJob.isCancelled} ${
      
      parentJob.isCompleted}")
Handler().postDelayed(Runnable {
    
    
    Log.e("子协程的状态" ,
        "${
      
      childJob?.isActive} ${
      
      childJob?.isCancelled} ${
      
      childJob?.isCompleted}")
    parentJob.cancel()
    Log.e("父协程的状态" ,
        "${
      
      parentJob.isActive} ${
      
      parentJob.isCancelled} ${
      
      parentJob.isCompleted}")
    Log.e("子协程的状态" ,
        "${
      
      childJob?.isActive} ${
      
      childJob?.isCancelled} ${
      
      childJob?.isCompleted}")
} , 1000)
打印结果如下:可以看到父协程取消以后,子协程也取消了。
父协程的状态: true false false
子协程的状态: true false false
父协程的状态: false true false
子协程的状态: false true false
3.4.4) CoroutineExceptionHandler coroutine exception handling

When we write code, we will definitely encounter abnormal situations. Normally we use try...catch to handle exceptions, but it is inevitable that omissions will occur. CoroutineExceptionHandler is a class that specializes in catching exceptions in coroutines. Exceptions that occur in coroutines will be caught and returned to us by the handleException method of CoroutineExceptionHandler for processing.

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    
    
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>

    public fun handleException(context: CoroutineContext, exception: Throwable)
}

handleException will return two parameters. The first parameter is the coroutine where the exception occurred, and the second parameter is the exception that occurred.
The example is as follows: we manually throw a NullPointerException exception, and then create a CoroutineExceptionHandler and assign it to the coroutine.

val exceptionHandler = CoroutineExceptionHandler {
    
     coroutineContext, throwable ->
    Log.e("捕获异常", "${
      
      coroutineContext[CoroutineName]}$throwable")
}

GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")+exceptionHandler) {
    
    
    Log.e("协程的coroutineContext",this.coroutineContext.toString())
    throw NullPointerException()
}
打印结果:
协程的coroutineContext: [CoroutineName(主协程), com.jf.simple.ThirdActivity$onCreate$$inlined$CoroutineExceptionHandler$1@288ff9, StandaloneCoroutine{
    
    Active}@b95b3e, Dispatchers.Main]

捕获异常: CoroutineName(主协程) :java.lang.NullPointerException
3.4.5) ContinuationInterceptor coroutine interceptor
public interface ContinuationInterceptor : CoroutineContext.Element {
    
    

    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}

As the name suggests, it is used to intercept coroutines. Because it involves the principle of suspending functions and is relatively rarely used in daily development, it will not be expanded here.

4. Startup mode of coroutine

When we look at the launch and async extension functions, there is a second parameter, start: CoroutineStart. The meaning of this parameter is the startup mode of the coroutine.

public enum class CoroutineStart {
    
    
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED;
}

You can see that CoroutineStart is an enumeration class with four types.
DEFAULT is the default startup mode. Scheduling starts immediately after the coroutine is created. Note that it is scheduled immediately rather than executed immediately. It may be canceled before execution.
LAZY lazy startup mode will not have any scheduling behavior after creation. Scheduling will not occur until we need it to execute. Scheduling starts only when we need to manually call the Job's start, join or await functions.
ATOMIC starts scheduling immediately after the coroutine is created, but it is different from the DEFAULT mode. In this mode, after the coroutine is started, it needs to execute to the first suspension point before responding to the cancel operation.
In this mode, the UNDISPATCHED coroutine will directly start executing in the current thread until it reaches the first suspension point. It is very similar to ATOMIC, but UNDISPATCHED is very affected by the scheduler.

Sample code:
DEFAULT: The code is printed immediately, indicating that the coroutine is scheduled immediately after creation

GlobalScope.launch {
    
    
    Log.e("default启动模式", "协程运行")
}

LAZY: Before the start() method is called, there is no printing. After the start() method is called, the code is printed. The coroutine description will not be scheduled after creation, and we need to start it manually.

val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
    
    
    Log.e("lazy启动模式", "协程运行")
}
//lazyJob.start()

ATOMIC: After the coroutine runs, it will not respond to the cancel() method until it reaches the first suspended function.

val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
    
    
    Log.e("atomic启动模式" , "运行到挂起函数前")
    delay(100)
    Log.e("atomic启动模式" , "运行到挂起函数后")
}
atomicJob.cancel()
打印结果:
atomic启动模式: 运行到挂起函数前

UNDISPATCHED: You can see that the results are very similar to ATOMIC, but because ATOMIC and UNDISPATCHED are relatively rarely used in development, we will not make too many distinctions here. If you are interested, you can open a sub-coroutine and add a scheduler to execute the code to see the difference.

5. Suspend function

We mentioned earlier: The biggest advantage of kotlin coroutines is to write asynchronous code in a synchronous manner, which is achieved through the suspension function.

The function modified by the keyword suspend is called a suspension function. The suspension function can only be called in the coroutine or another suspension function. The characteristic of the suspended function is "suspension and recovery". When the coroutine encounters a suspended function, the coroutine will be suspended. After the suspended function is executed, the coroutine will resume to the place where it was suspended and run again.
Suspension is a non-blocking suspension and does not block threads; recovery does not require us to manually recover, but the coroutine does it for us.

We demonstrate two examples here to understand the use of suspend functions. (I won’t talk about the principle of suspending functions here, I will start a special chapter later.)

5.1) Execute asynchronous code sequentially

Sample code 1: We define two suspension functions, one with a delay of 1s and one with a delay of 2s (to simulate a network request). In the end, they both return an integer, and we want to add the results. Without using coroutines, because the delays are different, we need to use a callback-like method to obtain the results. But using coroutines is different. We can see from the printing results that the code is executed completely sequentially. The measureTimeMillis method can measure the running time. We can see that the running time is a little more than 3s: So the whole process is like this. The coroutine runs to returnNumber1(), it is detected that it is a hanging function. The coroutine hangs and waits for returnNumber1() to complete. It takes 1s for returnNumber1() to complete. The coroutine returns to the place where returnNumber1() was called, obtains the result, and continues. Execution, the line reaches returnNumber2(), it is detected that it is a hanging function, the coroutine hangs, waiting for returnNumber2() to complete, it takes 2s to complete returnNumber2(), the coroutine returns to the place where returnNumber2() is called, and continues execution. .

suspend fun returnNumber1() : Int {
    
    
    delay(1000L)
    Log.e("returnNumber1" , "调用了returnNumber1()方法")
    return 1
}

suspend fun returnNumber2() : Int {
    
    
    delay(2000L)
    Log.e("returnNumber1" , "调用了returnNumber2()方法")
    return 2
}
GlobalScope.launch {
    
    
    val time = measureTimeMillis {
    
    
        val number1 = returnNumber1()
        Log.e("number1" , "需要获取number1")
        val number2 = returnNumber2()
        Log.e("number2" , "需要获取number2")
        val result = number1 + number2
        Log.e("执行完毕" , result.toString())
    }
    Log.e("运行时间",time.toString())
}
打印结果:
returnNumber1: 调用了returnNumber1()方法
number1: 需要获取number1
returnNumber1: 调用了returnNumber2()方法
number2: 需要获取number2
执行完毕: 3
运行时间: 3010
5.2) async implements concurrency

Note that the suspending function suspends the current coroutine and will not affect other coroutines.
Let’s modify the above code and place the two suspending functions in two sub-coroutines. The final result is obtained using await(). The function of the await() method is to wait for the coroutine to finish running and obtain the return result. We have mentioned the difference between launch and async before. One has a return value of the execution result, and the other does not, so async is used here.
Sample code: Async is used here to open two sub-coroutines. Both sub-coroutines have suspend functions, so both sub-coroutines will be suspended, but their parent coroutines are not suspended before calling the await() suspension function. It is not suspended, so it can run normally. The two sub-coroutines are executed concurrently. The final execution time can also be seen by printing. The code execution only takes 2 seconds.

GlobalScope.launch(Dispatchers.Main) {
    
    
    val time = measureTimeMillis {
    
    
        val deferred1 = async {
    
    
            Log.e("--" , "子协程1运行开始")
            returnNumber1()
        }
        Log.e("--" , "开始运行第二个协程")
        val deferred2 = async {
    
    
            Log.e("--" , "子协程2运行开始")
            returnNumber2()
        }
        Log.e("--" , "开始计算结果")

        val result = deferred1.await() + deferred2.await()
        Log.e("执行完毕" , result.toString())

    }
    Log.e("运行时间" , time.toString())
}

打印结果如下:
开始运行第二个协程
开始计算结果
子协程1运行开始
子协程2运行开始
returnNumber1: 调用了returnNumber1()方法
returnNumber1: 调用了returnNumber2()方法
执行完毕: 3
运行时间: 2009

In the next section, we will explain the principle of suspending functions in the most effective way.

Guess you like

Origin blog.csdn.net/weixin_43864176/article/details/126234790