The articles

Kotlin Jetpack In Action: The Beginning

00. Kotlin’s Pit Guide for Java Developers

03. The Triple Realm of Kotlin Programming

04. Kotlin’s Higher Order Functions

05. Kotlin Generics

06. The Kotlin Extension

07. Kotlin Commission

preface

Coroutines, the most amazing feature of Kotlin, are none of them.

This article will briefly introduce Kotlin coroutines and then explain how Kotlin coroutines work in the form of diagrams + animations. After reading this article, you’ll see that coroutines aren’t that hard after all.

1. Run the Demo while reading the article

Demo: github.com/chaxiu/Kotl…

2. Threads & coroutines

Some people think of coroutines as a wrapper framework for threads. In the grand scheme of things, this makes some sense, though Kotlin doesn’t officially publicize it.

From a microscopic perspective, coroutines are a bit like lightweight threads. How lightweight can coroutines be? If you create 1000 coroutines in one thread, it doesn’t matter.

From the perspective of inclusion, the coroutine to thread relationship is a bit like “thread to process relationship”, after all, coroutine cannot run without thread.

Coroutines cannot run outside of threads, but they can switch between threads. Now, you understand the meaning of the GIF shown at the beginning of this article?

So much for coroutines, but should we use them because they are “efficient” and “lightweight”? Assembly language is also very efficient. C can write lightweight programs.

Efficiency and lightweight are not the core competitiveness of Kotlin coroutines.

The core strength of Kotlin coroutines is that they simplifyAsynchronous concurrentTask.

As Java developers, we know how dangerous thread concurrency can be and how hard it is to write asynchronous code that is hard to maintain.

3. Asynchronous code & callback hell

For example, in Java code, we make an asynchronous request, query the user’s information from the server, and return response with a CallBack:

getUserInfo(new CallBack() {
    @Override
    public void onSuccess(String response) {
        if(response ! =null) { System.out.println(response); }}});Copy the code

So far, our code looks fine, but what if our requirements changed to this?

Query user information -> find the list of friends of the user -> get the list of friends, find the dynamic of the friend

getUserInfo(new CallBack() {
    @Override
    public void onSuccess(String user) {
        if(user ! =null) {
            System.out.println(user);
            getFriendList(user, new CallBack() {
                @Override
                public void onSuccess(String friendList) {
                    if(friendList ! =null) {
                        System.out.println(friendList);
                        getFeedList(friendList, new CallBack() {
                            @Override
                            public void onSuccess(String feed) {
                                if(feed ! =null) { System.out.println(feed); }}}); }}}); }}});Copy the code

It’s kind of gross, isn’t it? Again, this is just onSuccess, which is more complicated because we have exceptions to handle, retries to handle, thread scheduling to handle, and maybe even multithreaded synchronization to handle.

Hell to Heaven: coroutines

Today’s main role is coroutine, the above code with coroutine should be written? Very simple. The core is three lines of code:

val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)
Copy the code

Is it simple to the extreme? That’s the beauty of Kotlin coroutines: asynchronous tasks are done synchronously.

4-1 Key points of using coroutines

The key to writing the above code in a similar synchronous manner is the definition of the three request functions. Unlike normal functions, they are all labeled suspend, which means they are all: suspended functions.

// Delay (1000L) is used to simulate the network request

// Suspend the function
/ / left
suspend fun getUserInfo(a): String {
    withContext(Dispatchers.IO) {
        delay(1000L)}return "BoyCoder"
}
// Suspend the function
/ / left
suspend fun getFriendList(user: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)}return "Tom, Jack"
}
// Suspend the function
/ / left
suspend fun getFeedList(list: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)}return "{FeedList.. }"
}
Copy the code

So, what exactly is a hang function?

4-2 Suspend the function

Suspending functions are, literally, functions that can be suspended. Suspend n. Suspend n. Suspend n. Suspend N. In this context, it’s also kind of a pause. Pauses are easier to understand, but hangs are more accurate.

Suspend functions, which can be suspended and of course can be reinstated, usually come in pairs.

Let’s look at the execution of the suspended function and notice the flicker in the animation that indicates that the network is being requested.

Be sure to read it several times to make sure you haven’t missed anything.

From the animation above, we can see:

  • Code that appears to be synchronous actually involves thread switching.
  • One line of code, switching two threads.
  • =Left: main thread
  • =On the right: IO threads
  • From one anotherThe main threadtoIO threadIs a coroutinehang(suspend)
  • From one anotherIO threadtoThe main threadIs a coroutinerestore(resume).
  • Suspend and resume, this is the unique ability of suspend functions, ordinary functions do not have.
  • Suspend, simply transfer the program execution flow to another thread, the main thread is not blocked.
  • If the above code were running on Android, our App would still be responsive and the main thread would not be busy, which is easy to understand.

So, how does a Kotlin coroutine switch two threads in a single line of code?

The magic of all this is hidden in the suspend keyword.

The nature of Suspend

The essence of suspend is CallBack.

suspend fun getUserInfo(a): String {
    withContext(Dispatchers.IO) {
        delay(1000L)}return "BoyCoder"
}
Copy the code

A CallBack is a CallBack. It’s not. Indeed, we write code without a CallBack, but Kotlin’s compiler detects a function decorated by the suspend keyword and automatically converts the suspended function to a function with a CallBack.

If we decompiled the suspended function above into Java, the result would look like this:

// A Continuation is equivalent to a CallBack
/ / left
public static final Object getUserInfo(Continuation $completion) {...return "BoyCoder";
}
Copy the code

As a result of decompilation, the suspended function does become a function with a CallBack, but the real name of the CallBack is Continuation. After all, calling it a CallBack would be too low, right?

Let’s look at the definition of a Continuation in Kotlin:

public interface Continuation<in T> {
    public val context: CoroutineContext
// Equivalent to the onSuccess result
/ / left left
    public fun resumeWith(result: Result<T>)
}
Copy the code

Compare this to the definition of CallBack:

interface CallBack {
    void onSuccess(String response);
}
Copy the code

As you can see from the above definition, a Continuation is simply a CallBack with generic parameters, plus a CoroutineContext, which is the context for the coroutine. For those familiar with Android development, it’s context! It’s not too hard to understand, is it?

This conversion from a suspended function to a CallBack function is called a Continuation-Passing Style Transformation.

Here’s why Kotlin officially uses continuations instead of callbacks: continuations tell us how they work. Of course, in order to understand the suspended function, we use the CallBack to be more straightforward.

The following is an animated example of how the signature of a suspended function changes during CPS conversion:

This transformation looks simple, but there are some details hidden in it.

Changes in function types

Suspend ()->String: suspend ()-> Any? .

This means that if you access a Kotlin suspended function getUserInfo() in Java, you will see a Continuation of type getUserInfo() in Java :(Continuation)-> Object. (Receive a Continuation as an argument and return Object)

In this CPS transformation, suspend () becomes (Continuation), which is not difficult, as we explained earlier. So why did the return value change from: String to Any?

Suspend the return value of the function

After a suspend function is converted by CPS, its return value has an important function: it indicates whether the suspended function has been suspended or not.

This sounds a bit convoluted: a suspended function, which is a function that can be suspended, can it not be suspended? Yes, suspend functions can also not be suspended.

Let’s clear up a few concepts:

Suspend is a suspended function whenever it has the suspend modifier, as in our previous example:

suspend fun getUserInfo(a): String {
    withContext(Dispatchers.IO) {
        delay(1000L)}return "BoyCoder"
}
Copy the code

When getUserInfo () by withContext returns CoroutineSingletons. COROUTINE_SUSPENDED indicates that the function is put up.

Is this a suspended function?

Suspend fun noSuspendFriendList(user: String): String{return "Tom, Jack"}Copy the code

Answer: It’s a suspended function. But it differs from a normal suspended function: when it is executed, it will not be suspended because it is a normal function. When you write code like this, the IDE will also remind you that suspend is redundant:

When noSuspendFriendList() is called, it does not suspend, it simply returns a String: “No suspend”. You can think of a suspended function like this as a pseudo-suspended function

The return type is Any? The reason why

Due to suspend modified function, can return to CoroutineSingletons. COROUTINE_SUSPENDED, also may return “no suspend” actual results, and may even return null, in order to fit all the possibilities, If CPS is converted, the return type must be Any? .

summary

  • The suspend modifier isHang up function
  • Suspended functions are not always suspended when executed
  • A suspended function can only be called within another suspended function
  • A suspended function is suspended only when it contains other suspended functions

These are the details of the function signature in the CPS conversion process.

However, this is not the whole story of CPS conversion, because we don’t yet know what a Continuation really is.

6. The CPS transformation

Continuation is a word that, if you look it up in the dictionary or on Wikipedia, may confuse you. It’s so abstract.

Continuations are easier to understand with the example in our article.

First, we just need to keep track of the etymology of Continuation, Continue. A Continue is something that continues, and a Continuation is something that continues to be done, something that will be done next.

When you put it in a program, a Continuation represents the code that needs to be executed, the code that needs to be executed next, or the rest of the code.

For example, when the program runs getUserInfo(), its Continuation is the code in the red box below:

A Continuation is the code to run next, the remaining code that has not been executed.

Now that you understand continuations, CPS is easy to understand: a pattern of passing the code that your program is going to execute next.

The CPS conversion is the process of converting the original synchronous suspended function into the asynchronous code CallBack. This conversion is done behind the scenes by the compiler, and we programmers are unaware of it.

Some people may scoff: So simple? Do three suspended functions end up with three Callback functions?

Of course not. The thinking is still CPS thinking, but much smarter than Callback thinking.

Next, let’s see what the code looks like when the suspended function is decompiled. So much has been laid out, and it’s all in preparation for the next section.

7. Bytecode decompile

We’ve done bytecode decompilation into Java many times. Unlike usual, I won’t post the decompiled code directly, because if I did, it would probably scare a lot of people. Coroutine decompiled code, the logic is too convoluted, the readability is very poor. This kind of code, CPU may like, but really not people to read.

So, for the sake of your understanding, the next code I post is the approximate equivalent of my Kotlin translation, improving readability and eliminating unnecessary details. If you’ve figured out everything in this article, you’re already better at understanding coroutines than most people.

To get to the point, here’s what we’re going to look at, the testCoroutine code before decompilation:

suspend fun testCoroutine(a) {
    log("start")
    val user = getUserInfo()
    log(user)
    val friendList = getFriendList(user)
    log(friendList)
    val feedList = getFeedList(friendList)
    log(feedList)
}
Copy the code

When decompiled, the testCoroutine function’s signature looks like this:

// Completion is complete instead of suspend
fun testCoroutine(completion: Continuation<Any? >): Any? {}
Copy the code

Since several other functions are also suspended, their function signatures will also change:

fun getUserInfo(completion: Continuation<Any? >): Any? {}fun getFriendList(user: String, completion: Continuation<Any? >): Any? {}fun getFeedList(friendList: String, completion: Continuation<Any? >): Any? {}Copy the code

Next, let’s examine the body of testCoroutine(), because it’s quite complex, involving calls to three suspended functions.

First, within the testCoroutine function, there is an additional subclass of ContinuationImpl, which is the core of the entire coroutine suspend function. The comments in the code are very detailed, please read them carefully.

fun testCoroutine(completion: Continuation<Any? >): Any? {

    class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
        // represents the current state of the coroutine state machine
        var label: Int = 0
        // Coroutine returns the result
        var result: Any? = null

        // Used to save the results of previous coroutines
        var mUser: Any? = null
        var mFriendList: Any? = null

        // The invokeSuspend is the key to the coroutine
        // It eventually calls testCoroutine(this) to start the coroutine state machine
        // State machine related code is the following when statement
        // The essence of coroutines is CPS + state machine
        override fun invokeSuspend(_result: Result<Any? >): Any? {
            result = _result
            label = label or Int.Companion.MIN_VALUE
            return testCoroutine(this)}}}Copy the code

The next step is to determine whether testCoroutine is running for the first time, and if so, create an instance object of TestContinuation.

/ / left
fun testCoroutine(completion: Continuation<Any? >): Any? {...val continuation = if (completion is TestContinuation) {
        completion
    } else {
        // as an argument
        / / left
        TestContinuation(completion)
    }
}
Copy the code
  • The invokeSuspend will eventually call testCoroutine and then go to the judgment statement
  • If you are running it for the first time, a TestContinuation object is created, with completion as the argument
  • This is the equivalent of wrapping an old Continuation around a new one
  • If you are not running it for the first time, assign the Completion directly to the continuation
  • This shows that a continuation generates only one instance during its entire run, which is a significant memory savings (compared to a CallBack).

Here are the definitions of several variables, which are commented in detail in the code:

// Three variables, corresponding to the three variables of the original function
lateinit var user: String
lateinit var friendList: String
lateinit var feedList: String

// result Receives the run result of the coroutine
var result = continuation.result

SuspendReturn receives the return value from the suspended function
var suspendReturn: Any? = null

// CoroutineSingletons is an enumerated class
// COROUTINE_SUSPENDED means that the current function is suspended
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED
Copy the code

This brings us to the core logic of our state machine, as described in the comments:

when (continuation.label) {
    0- > {// Abnormal detection
        throwOnFailure(result)

        log("start")
        // Set the label to 1 to prepare for the next state
        continuation.label = 1

        / / getUserInfo execution
        suspendReturn = getUserInfo(continuation)

        // Determine whether to suspend
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            //go to next state}}1 -> {
        throwOnFailure(result)

        // Get the user value
        user = result as String
        log(user)
        // Store the coroutine result in a continuation
        continuation.mUser = user
        // Prepare to enter the next state
        continuation.label = 2

        / / getFriendList execution
        suspendReturn = getFriendList(user, continuation)

        // Determine whether to suspend
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            //go to next state}}2 -> {
        throwOnFailure(result)

        user = continuation.mUser as String

        // Get the value of friendList
        friendList = result as String
        log(friendList)

        // Store the coroutine result in a continuation
        continuation.mUser = user
        continuation.mFriendList = friendList

        // Prepare to enter the next state
        continuation.label = 3

        / / getFeedList execution
        suspendReturn = getFeedList(friendList, continuation)

        // Determine whether to suspend
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            //go to next state}}3 -> {
        throwOnFailure(result)

        user = continuation.mUser as String
        friendList = continuation.mFriendList as String
        feedList = continuation.result as String
        log(feedList)
        loop = false}}Copy the code
  • The when expression implements a coroutine state machine
  • continuation.labelIs the key to state flow
  • continuation.labelIf you change it once, you switch the coroutine once
  • After each coroutine switch, an exception is checked
  • The original code in testCoroutine isBreak upTo the states in the state machine,Performed separately
  • With a continuation call getUserInfo(User, continuation), getFriendList(User, continuation), and getFeedList(friendList, continuation)continuationInstance.
  • If a function is suspended, its return value will be:CoroutineSingletons.COROUTINE_SUSPENDED
  • Before switching coroutines, the state store the previous result as a member variable incontinuationIn the.

Warning: the above code is a modified version of the decompiled code I wrote with Kotlin, and I will show the actual code after the decompiled coroutine, please read on.

8. Coroutine state maneuver drawing demonstration

Does the string of text and code above look a little dizzy? Take a look at the animation. After watching the animation, go back and read the text. You will learn more.

Is it over? No, because the animation above demonstrates only the normal suspension of each coroutine. What if the coroutine doesn’t really hang? How does the coroutine state chance run?

The coroutine is not suspended

To verify this, we simply change one of the suspended functions to a pseudo-suspended function.

// "pseudo" suspend function
// Although it has the suspend modifier, it does not actually suspend when executed, because it has no other suspended functions in its body
/ / left
suspend fun noSuspendFriendList(user: String): String{
    return "Tom, Jack"
}

suspend fun testNoSuspend(a) {
    log("start")
    val user = getUserInfo()
    log(user)                  
    // The change is here
    / / left
    val friendList = noSuspendFriendList(user)
    log(friendList)
    val feedList = getFeedList(friendList)
    log(feedList)
}
Copy the code

What is the decompiled code logic for a function body such as testNoSuspend()?

The answer is simple. The structure of testCoroutine() is the same as that of testCoroutine(), except that the name of the function has been changed. The Kotlin compiler CPS conversion logic recognizes only the suspend keyword. The Kotlin compiler performs CPS conversions even if the suspended function is “pseudo.”

when (continuation.label) {
    0 -> {
        ...
    }

    1 -> {
        ...
        // The change is here
        / / left
        suspendReturn = noSuspendFriendList(user, continuation)

        // Determine whether to suspend
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            //go to next state}}2 -> {
        ...
    }

    3- > {... }}Copy the code

How does testNoSuspend() coroutine state machine run?

With suspendReturn == sFlag, suspendReturn == sFlag, suspendReturn == sFlag, suspendReturn == sFlag, suspendReturn == sFlag, suspendReturn == sFlag, suspendReturn == sFlag, suspendReturn == sFlag

Let’s see the specific differences through animation:

“SuspendReturn == sFlag” (‘ suspendReturn == sFlag ‘); “else” (‘ suspendReturn == sFlag ‘); “else” (‘ suspendReturn == sFlag ‘);

Now there’s only one more question:

if (suspendReturn == sFlag) {
} else {
    // How is the code implemented?
    / / left
    //go to next state
}
Copy the code

The answer is simple: if you look at the bytecode decompiled Java of the coroutine state machine, you’ll see a lot of labels. The underlying bytecode of the coroutine state machine implements this go to next state through the label. Since Kotlin doesn’t have a goto like syntax, I’ll use pseudocode to represent the logic of goto next state.

/ / pseudo code
// Kotlin has no such syntax
/ / left left
label: whenStart
when (continuation.label) {
    0 -> {
        ...
    }

    1 -> {
        ...
        suspendReturn = noSuspendFriendList(user, continuation)
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            // Let the program jump to where the label is
            // To execute the when expression again
            goto: whenStart
        }
    }

    2 -> {
        ...
    }

    3- > {... }}Copy the code

Note: This is pseudocode, and it’s just the logical equivalent of the coroutine state machine bytecode. I won’t explain the coroutine’s original bytecode here, so as not to ruin your fun of delving into coroutines. I’m sure that if you understand my article, then look at the actual code of coroutine decompilation, you will be comfortable.

The following suggestions will help you see the true bytecode of coroutines: Coroutine state machines really work by creating a state machine structure with nested label code segments and switches. The logic is complex and relatively difficult to understand. (After all, Java labels are rarely used in real development.)

The actual coroutine decompiled code would look something like this:

@Nullable
public static final Object testCoroutine(@NotNull Continuation $completion) {
    Object $continuation;
    label37: {
        if ($completion instanceof <TestSuspendKt$testCoroutine$1>) {
            $continuation = (<TestSuspendKt$testCoroutine$1>)$completion;
            if ((((<TestSuspendKt$testCoroutine$1>)$continuation).label & Integer.MIN_VALUE) ! =0) {
                ((<TestSuspendKt$testCoroutine$1>)$continuation).label -= Integer.MIN_VALUE;
                break label37;
            }
        }

        $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            int label;
            Object L$0;
            Object L$1;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
                this.result = $result;
                this.label |= Integer.MIN_VALUE;
                return TestSuspendKt.testCoroutine(this); }}; } Object var10000; label31: { String user; String friendList; Object var6; label30: { Object $result = ((<TestSuspendKt$testCoroutine$1>)$continuation).result;
            var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(((<TestSuspendKt$testCoroutine$1>)$continuation).label) {
                case 0:
                    ResultKt.throwOnFailure($result);
                    log("start");
                    ((<TestSuspendKt$testCoroutine$1>)$continuation).label = 1;
                    var10000 = getUserInfo((Continuation)$continuation);
                    if (var10000 == var6) {
                        return var6;
                    }
                    break;
                case 1:
                    ResultKt.throwOnFailure($result);
                    var10000 = $result;
                    break;
                case 2:
                    user = (String)((<TestSuspendKt$testCoroutine$1>)$continuation).L$0;
                    ResultKt.throwOnFailure($result);
                    var10000 = $result;
                    break label30;
                case 3:
                    friendList = (String)((<TestSuspendKt$testCoroutine$1>)$continuation).L$1;
                    user = (String)((<TestSuspendKt$testCoroutine$1>)$continuation).L$0;
                    ResultKt.throwOnFailure($result);
                    var10000 = $result;
                    break label31;
                default:
                    throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            user = (String)var10000;
            log(user);
            ((<TestSuspendKt$testCoroutine$1>)$continuation).L$0 = user;
            ((<TestSuspendKt$testCoroutine$1>)$continuation).label = 2;
            var10000 = getFriendList(user, (Continuation)$continuation);
            if (var10000 == var6) {
                return var6;
            }
        }

        friendList = (String)var10000;
        log(friendList);
        ((<TestSuspendKt$testCoroutine$1>)$continuation).L$0 = user;
        ((<TestSuspendKt$testCoroutine$1>)$continuation).L$1 = friendList;
        ((<TestSuspendKt$testCoroutine$1>)$continuation).label = 3;
        var10000 = getFeedList(friendList, (Continuation)$continuation);
        if (var10000 == var6) {
            return var6;
        }
    }

    String feedList = (String)var10000;
    log(feedList);
    return Unit.INSTANCE;
}
Copy the code

9. The ending

Looking back at the relationship between threads and coroutines:

thread

  • Threads are an operating system level concept
  • The threads that we developers create in a programming language (thread.java) are essentially a mapping of the operating system kernel threads
  • JVM threads are mapped to kernel threads, such as “one to one”, “one to many”, and “M to N”. JVM implementations vary across operating systems, with “one to one” being the mainstream
  • In general, when we talk about threads, we’re talking about kernel threads, and switching between threads, scheduling, is all up to the operating system
  • Threads also consume operating system resources, but they are much lighter than processes
  • Threads, which are preemptive, can share memory resources, but processes cannot
  • Thread sharing causesMultithreaded synchronization problem
  • Some programming languages implement their own threading libraries to enable multi-threading in a single kernel thread, such as the “green threads” of early JVMS, which are called “user threads”

Some people compare threads to lightweight processes.

coroutines

  • Kotlin coroutines, not an operating system level concept, require no operating system support
  • Kotlin coroutines, kind of like the “green thread” mentioned above, can run thousands of coroutines on a single thread
  • Kotlin coroutines are userlevel, and the kernel is not aware of the coroutines
  • Kotlin coroutines, which are collaborative and managed by developers, do not require OS scheduling and switching, and there is no preemptive cost, so it is more efficient
  • Kotlin coroutines, whose underlying implementation is based on a state machine, share a single instance between multiple coroutines and have minimal resource overhead, so it is more lightweight
  • Kotlin coroutines, again, essentially run on threads, and they can run on different threads through the coroutine scheduler

Hang functions are the most important part of Kotlin coroutines and must be understood thoroughly.

All the code in this article has been provided in the Demo, after reading this article, you must have a lot of doubts, be sure to go to the actual debugging run: github.com/chaxiu/Kotl…