Ali P7 boss teaches you to crack the Kotlin coroutine (3) - coroutine scheduling

Cracking Kotlin Coroutine (3) - Coroutine Scheduling

1. Coroutine context

The scheduler is essentially the implementation of a coroutine context. Let's introduce the context first.

We mentioned earlier that launchthe function has three parameters, the first parameter is called context , and its interface type is CoroutineContext, usually the type of context we see is CombinedContextor EmptyCoroutineContext, one represents a combination of contexts, and the other represents nothing. Let's look at the interface methods CoroutineContextof :

@SinceKotlin("1.3")
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 {
        public val key: Key<*>
        ...
    }
}

I don’t know if you have noticed, it is simply an index Keybased List:

|
CoroutineContext

|

List

|
| — | — |
|

get(Key)

|

get(Int)

|
|

plus(CoroutineContext)

|

plus(List)

|
|

minusKey(Key)

|

removeAt(Int)

|

in the table List.plus(List)actually refers to the extension methodCollection<T>.plus(elements:Iterable<T>):List<T>

CoroutineContextAs a collection, its elements are the ones seen in the source code Element, each Elementhas one key, so it can appear as an element, and it is also a sub-interface CoroutineContextof , so it can also appear as a collection.

Speaking of this, everyone will understand that CoroutineContextit turns out to be a data structure. If you are familiar with the recursive definition Listof , then itCombinedContext is easy to understand and , for example, scala's is defined like this:EmptyCoroutineContextList

sealed abstract class List[+A] extends ... {
    ...
    def head: A
    def tail: List[A]
    ...
}

When the pattern is matched, List(1,2,3,4)it can be x::ymatched , xand it is 1, yso it is List(2,3,4).

CombinedContextThe definition of is also very similar:

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    ...
}

It's just that it's reversed, the front is a collection, and the back is a single element. coroutineContextMost of what we access in the coroutine body is this CombinedContexttype, which means that there are many sets of specific context implementations. If we want to find a specific context implementation, we need to use the corresponding Keyto find it, for example:

suspend fun main(){
    GlobalScope.launch {
        println(coroutineContext[Job]) // "coroutine#1":StandaloneCoroutine{Active}@1ff62014
    }
    println(coroutineContext[Job]) // null,suspend main 虽然也是协程体,但它是更底层的逻辑,因此没有 Job 实例
}

Here is Jobactually companionobjecta reference to its

public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job> { ... }
    ...
}

So we can also imitate Thread.currentThread()to come up with a Jobmethod to get the current:

suspend inline fun Job.Key.currentJob() = coroutineContext[Job]

suspend fun coroutineJob(){
    GlobalScope.launch {
        log(Job.currentJob())
    }
    log(Job.currentJob())
}

We can add some features to the coroutine by specifying the context. A good example is to add a name to the coroutine to facilitate debugging:

GlobalScope.launch(CoroutineName("Hello")) {
    ...
}

copy

If there are multiple contexts to add, +just :

GlobalScope.launch(Dispatchers.Main + CoroutineName("Hello")) {
    ...
}

Dispatchers.Mainis an implementation of the scheduler, don't worry, we'll get to know it shortly.

2. Coroutine interceptor

After spending a lot of time talking about the context, here is a special existence-interceptors.

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

    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    ...
}

Interceptor is also the implementation direction of a context. Interceptor can control the execution of your coroutine. At the same time, in order to ensure the correctness of its function, the coroutine context collection will always put it at the end. This is really the chosen one. .

Its method of intercepting coroutines is also very simple, because the essence of coroutines is callback + "black magic", and this callback is Continuationintercepted . Friends who have used OkHttp will be excited immediately. I often use interceptors. OkHttp uses interceptors for caching, logging, and simulating requests. The same is true for coroutine interceptors. The scheduler is implemented based on the interceptor, in other words, the scheduler is a kind of interceptor.

We can define an interceptor and put it in our coroutine context to see what happens.

class MyContinuationInterceptor: ContinuationInterceptor{
    override val key = ContinuationInterceptor
    override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}

class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
    override val context = continuation.context
    override fun resumeWith(result: Result<T>) {
        log("<MyContinuation> $result" )
        continuation.resumeWith(result)
    }
}

We just hit a log line at the callback. Next we take out the use case:

suspend fun main() {
    GlobalScope.launch(MyContinuationInterceptor()) {
        log(1)
        val job = async {
            log(2)
            delay(1000)
            log(3)
            "Hello"
        }
        log(4)
        val result = job.await()
        log("5. $result")
    }.join()
    log(6)
}

This is probably the most complicated example we've given so far, but please don't be intimidated by it, it's still pretty simple. We launchstarted , specified our own interceptor as the context for it, and then asyncstarted a coroutine asyncwith in it, which is the same type of function launchfrom the function, and they are all called the builder function of the coroutine , the difference is that asyncthe activated Jobis the actual Deferredcan have a return result, which can awaitbe obtained through the method.

It is conceivable that resultthe value of is Hello. So what is the result of running this program?

15:31:55:989 [main] <MyContinuation> Success(kotlin.Unit)  // ①
15:31:55:992 [main] 1
15:31:56:000 [main] <MyContinuation> Success(kotlin.Unit) // ②
15:31:56:000 [main] 2
15:31:56:031 [main] 4
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(kotlin.Unit) // ③
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] 3
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(Hello) // ④
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 5. Hello
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 6

"// ①" is not the content output by the program, it is only marked for the convenience of subsequent explanations.

Everyone may be wondering, didn’t you say Continuationthat is a callback, and there is only one callback call ( awaitthere), why is the log printed four times?

Don't panic, we will introduce you in order.

First of all, when all coroutines start, there will be Continuation.resumeWithan operation. This operation is a scheduling opportunity for the scheduler. This is the key to our coroutines having the opportunity to schedule to other threads. This is the case in both ① and ②.

Secondly, delayit is the suspension point. After 1000ms, the coroutine needs to be scheduled and executed, so there is a log at ③.

Finally, the log at ④ is easy to understand, it is our return result.

Some friends may still have questions. I didn't switch threads in the interceptor. Why is there a thread switching operation starting from ③? The logic of switching threads comes from the fact delaythat on the JVM delay, ScheduledExcecutora delayed task is actually added in a , so thread switching will occur; while in the JavaScript environment, it is based on setTimeout. If it runs on Nodejs, it will delaynot Cut the thread, after all, people are single-threaded.

If we handle the thread switching ourselves in the interceptor, then we have implemented a simple scheduler of our own. If you are interested, you can try it yourself.

Thinking: Can there be more than one interceptor?

3. Scheduler

3.1 Overview

With the previous foundation, our introduction to the scheduler becomes a matter of course.

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ...
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    ...
}

It itself is a subclass of the coroutine context, and at the same time implements the interface of the interceptor, and dispatchthe method will be called interceptContinuationin , thereby realizing the scheduling of the coroutine. So if we want to implement our own scheduler, we can inherit this class, but usually we use ready-made ones, which are defined Dispatchersin :

val Default: CoroutineDispatcher
val Main: MainCoroutineDispatcher
val Unconfined: CoroutineDispatcher

The definition of this class involves the support of Kotlin MPP, so you will also see it in the Jvm version val IO:CoroutineDispatcher. In js and native, there are only the three mentioned above (I am partial to Jvm).

|
|

Jvm

|

Js

|

Native

|
| — | — | — | — |
|

Default

|

Thread Pool

|

main thread loop

|

main thread loop

|
|

Main

|

UI thread

|

Same as Default

|

Same as Default

|
|

Unconfined

|

direct execution

|

direct execution

|

direct execution

|
|

IO

|

Thread Pool

|

|

|

  • IO is only defined on the Jvm. It is based on the thread pool behind the Default scheduler and implements independent queues and limits. Therefore, switching the coroutine scheduler from Default to IO does not trigger thread switching.
  • Main is mainly used for UI-related programs, including Swing, JavaFx, and Android on Jvm, and can dispatch coroutines to their respective UI threads.
  • Js itself is a single-threaded event loop, which is similar to the UI program on the Jvm.

3.2 Write UI-related programs

The vast majority of Kotlin users are Android developers, and everyone has a relatively large demand for UI development. Let's take a very common scenario, click a button to do some asynchronous operations and then call back to refresh the UI:

getUserBtn.setOnClickListener { 
    getUser { user ->
        handler.post {
            userNameView.text = user.name
        }
    }
}

We simply give the declaration of getUserthe function :

typealias Callback = (User) -> Unit

fun getUser(callback: Callback){
    ...
}

Since getUserthe function needs to be switched to other threads for execution, the callback is usually called in this non-UI thread, so in order to ensure that the UI is refreshed correctly, we need to use handler.postto switch to the UI thread. The above writing method is our oldest writing method.

Then came RxJava, and things started to get interesting:

fun getUserObservable(): Observable<User> {
    return Observable.create<User> { emitter ->
        getUser {
            emitter.onNext(it)
        }
    }
}

So the button click event can be written as follows:

getUserBtn.setOnClickListener {
    getUserObservable()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { user ->
                userNameView.text = user.name
            }
}

In fact, RxJava's performance in thread switching is very good, and it is exactly the same. Many people even use it just for the convenience of thread switching!

So now we transition this code to the coroutine writing method:

suspend fun getUserCoroutine() = suspendCoroutine<User> {
    continuation ->
    getUser {
        continuation.resume(it)
    }
}

On button click, we can:

getUserBtn.setOnClickListener {
    GlobalScope.launch(Dispatchers.Main) {
        userNameView.text = getUserCoroutine().name
    }
}

You can also use the View.onClick extension in anko-coroutines, so we don't need to launchstart . Regarding Anko's support for coroutines, we will arrange an article to introduce them later.

Here is something you haven’t seen before. suspendCoroutineThis method does not help us start the coroutine. It runs in the coroutine and helps us get Continuationthe instance , that is, get the callback, which is convenient for us to call it later. The resumeor resumeWithExceptionto return a result or throw an exception.

If you call repeatedly resumeor resumeWithExceptionget a coin IllegalStateException, think about why.

Compared with the previous RxJava approach, you will find that this code is actually very easy to understand, and you will even find that the usage scenarios of coroutines are so similar to RxJava. Here we use Dispatchers.Mainto ensure launchthat the coroutine started by is always scheduled to the UI thread when scheduling, so let's take a look at the specific implementation Dispatchers.Mainof .

On the Jvm, Mainthe implementation of is also more interesting:

internal object MainDispatcherLoader {
    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return try {
            val factories = MainDispatcherFactory::class.java.let { clz ->
                ServiceLoader.load(clz, clz.classLoader).toList()
            }
            factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: MissingMainCoroutineDispatcher(null)
        } catch (e: Throwable) {
            MissingMainCoroutineDispatcher(e)
        }
    }
}

In Android, the coroutine framework registers AndroidDispatcherFactoryso that is Mainfinally assigned an instance HandlerDispatcherof . If you are interested, you can check the source code implementation of kotlinx-coroutines-android.

Note that in the previous implementation of RxJava and coroutines, we did not consider exceptions and cancellations. The topic of exceptions and cancellation will be covered in detail in a later article.

3.3 Schedulers bound to arbitrary threads

The purpose of the scheduler is to cut threads. Don't think that I will randomly call it according to my mood when I am dispatchin , then you are harming yourself (don't be afraid of your jokes, I really wrote such code, just for entertainment). Then the problem is simple, as long as we provide threads, the scheduler should be easily created:

suspend fun main() {
    val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()
    GlobalScope.launch(myDispatcher) {
        log(1)
    }.join()
    log(2)
}

The output information indicates that the coroutine is running on our own thread.

16:10:57:130 [MyThread] 1
16:10:57:136 [MyThread] 2

But please note that since this thread pool is created by ourselves, we need to close it at the right time, otherwise:

We can actively close the thread pool or call:

myDispatcher.close()

To end its life cycle, run the program again and it will exit normally.

Of course, some people will say that the threads in the thread pool you created are not daemon, so the Jvm will not stop running when the main thread ends. You are right, but what should be released should be released in time. If you only use this scheduler for a short time in the entire life cycle of the program, won't there be thread leaks if you don't close its corresponding thread pool all the time? That's embarrassing.

Kotlin coroutine designers are also very afraid that people will not notice this, and they deliberately abandoned two APIs and opened an issue saying that we want to redo this set of APIs. Who are these two poor guys?

Two abandoned APIs for creating schedulers based on thread pools

fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher
fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher

These two can be very convenient to create a scheduler bound to a specific thread, but the overly concise API seems to make people forget its risk. Kotlin has never liked to do such unclear things, so you should construct the thread pool by yourself as in the example in this section, so that you forget to close it anyway and you can’t blame others (hahaha).

In fact, running coroutines on multiple threads, the threads are always cut like this is not very lightweight. For example, the following example is quite scary:

Executors.newFixedThreadPool(10)
        .asCoroutineDispatcher().use { dispatcher ->
            GlobalScope.launch(dispatcher) {
                log(1)
                val job = async {
                    log(2)
                    delay(1000)
                    log(3)
                    "Hello"
                }
                log(4)
                val result = job.await()
                log("5. $result")
            }.join()
            log(6)
        }

Except delayfor an unavoidable thread switch, the continuation operations ( Continuation.resume) at the suspension points of other coroutines will cut threads:

16:28:04:771 [pool-1-thread-1] 1
16:28:04:779 [pool-1-thread-1] 4
16:28:04:779 [pool-1-thread-2] 2
16:28:05:790 [pool-1-thread-3] 3
16:28:05:793 [pool-1-thread-4] 5. Hello
16:28:05:794 [pool-1-thread-4] 6

If our thread pool only opens 1 thread, then all output here will be printed in this only thread:

16:40:14:685 [pool-1-thread-1] 1
16:40:14:706 [pool-1-thread-1] 4
16:40:14:710 [pool-1-thread-1] 2
16:40:15:723 [pool-1-thread-1] 3
16:40:15:725 [pool-1-thread-1] 5. Hello
16:40:15:725 [pool-1-thread-1] 6

Comparing the two, in the case of 10 threads, the number of thread switches is at least 3 times, while in the case of 1 thread, it only needs to delaybe executed once after 1000ms. Just two more thread switches, how much impact will it have? I ran the loop 100 times for two different cases on my own 2015 mbp and got the following average times:

|
Number of threads

|

10

|

1

|
| — | — | — |
|

Time-consuming ms

|

1006.00

|

1004.97

|

Note that for the fairness of the test, a warm-up has been done before running the loop 100 times to ensure that all classes have been loaded. Test results are for reference only.

That is to say, two more thread switches can take an average of 1ms more time-consuming. The code in the production environment will of course be more complicated. If you use the thread pool to schedule in this way, the result can be imagined.

In fact, usually we only need to process our own business logic in one thread, and only some time-consuming IO needs to be switched to the IO thread for processing, so a good practice can refer to the scheduler corresponding to the UI, and define the scheduler through the thread pool by yourself There is nothing wrong with the approach itself, but it is best to use only one thread, because multi-threading has thread safety issues in addition to the overhead of thread switching mentioned above.

3.4 Thread Safety Issues

The concurrency model of Js and Native is different from Jvm. Jvm exposes the thread API to users, which also makes the scheduling of coroutines more flexible for users to choose. More freedom means more cost. What we need to understand when writing coroutine code on Jvm is that thread safety issues still exist between different coroutines of the scheduler.

A good practice, as we mentioned in the previous section, is to try to control your own logic within one thread, which saves the cost of thread switching on the one hand, and avoids thread safety issues on the other hand, which is the best of both worlds.

If you use concurrency tools such as locks in the coroutine code, it will increase the complexity of the code. My suggestion for this is that you try to avoid referencing variable variables in the external scope when writing coroutine code. Use parameter passing instead of references to global variables.

The following is an example of a mistake, which is easy for everyone to figure out:

suspend fun main(){
    var i = 0
    Executors.newFixedThreadPool(10)
            .asCoroutineDispatcher().use { dispatcher ->
                List(1000000) {
                    GlobalScope.launch(dispatcher) {
                        i++
                    }
                }.forEach {
                    it.join()
                }
            }
    log(i)
}

Output result:

16:59:28:080 [main] 999593

4. How to schedule the suspend main function?

In the previous article, we mentioned that suspend main will start a coroutine. The coroutines in our example are all its sub-coroutines, but how did this outermost coroutine come about?

Let's give an example first:

suspend fun main() {
    log(1)
    GlobalScope.launch {
        log(2)
    }.join()
    log(3)
}

It is equivalent to writing the following:

fun main() {
    runSuspend {
        log(1)
        GlobalScope.launch {
            log(2)
        }.join()
        log(3)
    }
}

Then why runSuspenddo is sacred? It is a method of the Kotlin standard library, note that it is not in kotlinx.coroutines, it actually belongs to a lower-level API.

internal fun runSuspend(block: suspend () -> Unit) {
    val run = RunSuspend()
    block.startCoroutine(run)
    run.await()
}

And RunSuspendhere is Continuationthe implementation of :

private class RunSuspend : Continuation<Unit> {
    override val context: CoroutineContext
        get() = EmptyCoroutineContext

    var result: Result<Unit>? = null

    override fun resumeWith(result: Result<Unit>) = synchronized(this) {
        this.result = result
        (this as Object).notifyAll()
    }

    fun await() = synchronized(this) {
        while (true) {
            when (val result = this.result) {
                null -> (this as Object).wait()
                else -> {
                    result.getOrThrow() // throw up failure
                    return
                }
            }
        }
    }
}

Its context is empty, so the coroutine started by suspend main does not have any scheduling behavior.

Through this example, we can know that actually starting a coroutine only needs a lambda expression. When Kotlin 1.1 was first released, I wrote a series of tutorials based on the standard library API. Later, I found that the API of the standard library may not really be used by us, so just take a look.

The above codes are decorated in the standard library internal, so we cannot use them directly. However, you can copy the content of RunSuspend.kt to your project, so that you can use it directly, and the varresult:Result<Unit>?=nullmay report an error, it doesn't matter, privatevarresult:Result<Unit>?=nulljust change it to .

5. Summary

In this article, we introduced the coroutine context, introduced the interceptor, and finally led to our scheduler. So far, we have not talked about exception handling, coroutine cancellation, Anko's support for coroutines, etc. Yes, if you have any topics related to coroutines that you want to know, you can leave a message~

More Kotlin learning materials can be scanned for free!

The Kotlin Getting Started Tutorial Guide

Chapter 1 Kotlin Getting Started Tutorial Guide

​ ● Preface

img

Chapter 2 Overview

​ ● Server-side development with Kotlin

​ ● Android development with Kotlin

​ ● Kotlin JavaScript Overview

​ ● Kotlin/Native for native development

​ ● Coroutines for scenarios such as asynchronous programming

​ ● New features in Kotlin 1.1

​ ● New features in Kotlin 1.2

​ ● New features in Kotlin 1.3

img

Chapter 3 begins

​ ● Basic syntax

​ ● Idioms

​ ● Coding Standards

img

Chapter 4 Basics

​ ● Basic types

​ ● package

​ ● Control flow: if, when, for, while

​ ● Back and jump

img

Chapter 5 Classes and Objects

​ ● Classes and Inheritance

​ ● Attributes and fields

​ ● interface

​ ● Visibility modifiers

​ ● Extension

​ ● data class

​ ● Sealed

​ ● Generics

​ ● Nested classes and inner classes

​ ● Enumeration class

​ ● Object expressions and object declarations

​ ● Inline classes

​ ● commission

delegated property

img

Guess you like

Origin blog.csdn.net/Android_XG/article/details/130007935