Kotlin coroutine implementation principle: CoroutineScope, if you don't understand after reading it, you cut me! Wall cracks are recommended for collection.

Today we will talk about Kotlinthe coroutine Coroutine. At the end of the article there is an easter egg for everyone.

If you have not been in contact with coroutines, I recommend you to read this entry-level article What? Do you still know Kotlin Coroutine?

If you have contacted the coroutine, I believe you have had the following questions:

  1. What exactly is a coroutine?
  2. suspendWhat is the role of the coroutine and how does it work?
  3. Coroutine some of the key name (for example: Job, Coroutine, Dispatcher, CoroutineContextand CoroutineScope) in the end is kind of how the relationship between them?
  4. What is the so-called non-blocking suspend and resume of the coroutine?
  5. What is the internal implementation principle of the coroutine?

The following articles try to analyze these questions, and everyone is welcome to join in and discuss.

What is a coroutine

This question is very simple, as long as you are not in contact with the coroutine, you should be able to know. Because the definition is clearly given in the official document.

Let's take a look at the original official words (also the most emboldened paragraph in this article).

Coroutine is a concurrent design pattern, you can use it on the Android platform to simplify the asynchronous code execution.

Knock on the blackboard and draw the key points: Coroutine is a concurrent design pattern.

So it's not another manifestation of what some people say about threads. Although threads are also used internally in the coroutine. But its greater role is its design philosophy. CallbackEliminate our traditional callback method. Move asynchronous programming closer to synchronous alignment.

After so many explanations, in the end we still point directly to see its advantages

  1. Lightweight: You can run multiple coroutines on a single thread, because the coroutine supports suspend and will not block the thread that is running the coroutine. Suspending saves memory compared to blocking and supports multiple parallel operations.
  2. Less memory leaks: Use structured concurrency mechanisms to perform multiple operations in a scope.
  3. Built-in cancellation support: The cancellation function will automatically propagate through the running coroutine hierarchy.
  4. Jetpack integration: Many Jetpack libraries include extensions that provide full coroutine support. Some libraries also provide their own coroutine scope, which you can use for structured concurrency.

suspend

suspendIt is the key word of the coroutine, each suspendmodified method must be called in another suspendfunction or Coroutinecoroutine program.

The first time I saw this definition, I don’t know if you have any doubts. Anyway, I’m wondering if I have a nap. Why do the suspendmodification methods need to have this limitation? Why can't you not add it? What is its function?

Of course, if you pay attention to my previous article, you should know something about it, because I have briefly mentioned it in this article on reviewing the Retrofit source code and laughing at the coroutine implementation .

Here is a common name for a mechanism CPS(Continuation-Passing-Style). Each suspendmodified method or lambdaexpression will add additional Continuationtype parameters to it when the code is called .

@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse

The CPSreal face of the above code after conversion is like this

@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?

After the conversion, the original return type NewsResponseis added to the new Continutationparameter, and the Any?type is returned at the same time . There may be questions here? The return type is changed, and the result is wrong?

Actually not, Any?in Kotlinrather special, it can represent any type.

When a suspendfunction is suspended by the coroutine, it will return a special identifier COROUTINE_SUSPENDED, which is essentially one Any; when the coroutine is not suspended for execution, it will return the result of the execution or the exception raised. In this way, in order to support the return in both cases, a Kotlinunique Any?type is used.

The return value is understood, now let's talk about this Continutationparameter.

First look at Continutationthe source code

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

contextIt is the context of a coroutine. It is more often a CombinedContexttype, similar to a collection of coroutines. This will be explained in detail later.

resumeWithIt is used to wake up the suspended coroutine. As mentioned earlier, in the process of execution of the coroutine, in order to prevent blocking, the suspend feature is used. Once the logic inside the coroutine is executed, this method is used to invoke the coroutine. Let it continue execution where it was previously suspended.

So every suspendmodified function will get the upper level Continutationand pass it to itself as a parameter. Since it is passed from the upper layer, Continutationwho created it?

In fact, it Continutationis not difficult to guess that it was created with the coroutine when it was created.

GlobalScope.launch { 

}

launchAt that time, the Continutationobject has been created and the coroutine has been started. So the parameters passed by the suspended coroutine in it are all this object.

The simple understanding is that the use of coroutines resumeWithreplaces the traditional method callback. Every coroutine program will be created Continutation, and the coroutine will be automatically Continutationcalled back once when it is created resumeWith, so that the coroutine can start to execute.

CoroutineContext

The context of the coroutine, which contains some user-defined data collections, which are closely related to the coroutine. It is similar to a mapcollection and can be keyused to obtain different types of data. At the same time CoroutineContext, it is very flexible. If it needs to be changed, just use the current one CoroutineContextto create a new one CoroutineContext.

Look at CoroutineContextthe definition

public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?

    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...

    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext

    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {..}
}

Each one CoroutineContexthas its only one Keytype is Elementthat we can Keyget the corresponding specific object through the corresponding . It's a bit abstract, let's understand it directly through examples.

var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]

Job, DispatchersAnd CoroutineNamehave realized the Elementinterface.

If you need to combine different ones, CoroutineContextyou can go directly through +splicing, the essence is to use the plusmethod.

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    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)
                    }
                }
            }

plusThe realization logic of is to CoroutineContextencapsulate two splicing CombinedContextinto a splicing chain, and at the same time it will be ContinuationInterceptoradded to the end of the splicing chain each time .

So CombinedContextwhat is it?

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

    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {
            cur.element[key]?.let { return it }
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return next[key]
            }
        }
    }
    ...
}

Pay attention to its two parameters, we directly take the above example to analyze

Job() + Dispatchers.IO
(Job, Dispatchers.IO)

JobCorresponds to left, Dispatchers.IOcorrespondence element. If you splice another layer CoroutineName(aa), it's like this

((Job, Dispatchers.IO),CoroutineName)

The function is similar to the linked list, but the difference is that you can get the whole content connected to you . It is the corresponding minusKeymethod to remove the mapping from the set Keyof CoroutineContextinstances.

With this foundation, the getmethod we look at it will be very clear. Take it first element, not take it again left.

So Keywhat exactly is this? Let's take a lookCoroutineName

public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>

    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

It's very simple Keyis CoroutineContext.Key<CoroutineName>, of course, this is not enough, need to continue to combine the operator getmethod, so we'll look at Elementthe getmethods

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

As used herein, to Kotlinthe operatoroperator overloading characteristics. Then the following code is equivalent.

context.get(CoroutineName)
context[CoroutineName]

So we can directly analogous Mapto acquire the entire coroutine manner CoroutineContextset corresponding to Keythe CoroutineContextexample.

This article mainly introduces the suspendworking principle and CoroutineContextinternal structure. I hope it will be helpful to the partners who study the coroutine, so stay tuned for the follow-up coroutine analysis.

project

android_startup : Provides a simpler and more efficient way to initialize components when the application starts, and optimize the startup speed. Not only supports Jetpack App Startupall the functions, but also provides additional synchronization and asynchronous waiting, thread control and multi-process support.

AwesomeGithub : GithubClient- based , pure exercise project, support component development, support account password and authentication login. Use Kotlinlanguage development, project architecture is based on Jetpack&DataBindingthe MVVM; the project uses Arouter, Retrofit, Coroutine, Glide, Daggerand Hiltother open source technologies popular.

flutter_github : Based on Flutterthe cross-platform version of the Githubclient, AwesomeGithubcorresponding to it.

android-api-analysis : Combined with detailed Demoanalysis of Androidrelevant knowledge points, it helps readers to grasp and understand the points expounded more quickly.

daily_algorithm : A daily algorithm, from shallow to deep, welcome to join and encourage each other.

Kotlin learns from zero to the whole system

Why learn Kotlin? The future trend? Is it really fragrant?

I personally think that many Kotlin features make the code more concise and understandable than Java without sacrificing performance or security.
Kotlin is compiled to bytecode, so its performance is as good as Java. It has the same compile-time checks as Java (and more, such as built-in nullability checks). Most importantly, Kotlin's language functions and standard library functions can achieve concise and effective code.

Concise, because this is a key factor in improving programmer productivity.

Initially it was assembled. Each line of code only provides you with a functional description of the entire program. This makes reading and writing difficult, because you have to keep so much code in your mind at once.

High-level languages ​​allow us to add more ideas to each line of code. For example, sorting a list is trivial in most modern languages. When each line of code gains more functionality, it is easier to write larger programs.

However, we don't want to include as many ideas as possible in each line of code. If the code is too concise, it will also become difficult to understand. Regular expressions are a typical example of this problem. In short, I still fight it regularly.

In summary, uphold the love of Kotlin, and hope that more friends can get started with Kotlin better and painlessly. The compressed package in the picture of "Kotlin from Zero to Whole Study Notes" collected and compiled is a large number of actual Kotlin cases. , Share it with everyone for free. You can also find the update address of my Android high-level instructional video at station B on my homepage


Friends in need can leave your email address in the comment area. I will take the time to read the comments regularly and send them to everyone. If the reply is not timely, you can also add my WeChat to the introduction of my homepage, remark [KT] free sharing

Guess you like

Origin blog.csdn.net/Androiddddd/article/details/109723827