Skip to content

Instantly share code, notes, and snippets.

@chaxiu
Created October 18, 2020 12:04
Show Gist options
  • Save chaxiu/d87870528bbfe3e9d7e481e1f6acace1 to your computer and use it in GitHub Desktop.
Save chaxiu/d87870528bbfe3e9d7e481e1f6acace1 to your computer and use it in GitHub Desktop.

Diagram Coroutines: suspend

1. Preface

Coroutines is the most amazing feature in Kotlin.

In this post, I'll briefly introduce Kotlin coroutines, and then explain Coroutines in the form of Diagram + Animation. After reading this post, you may find out that Coroutines is not that difficult.

Coroutines-dispatch

2. Preparation

3. Thread & Coroutines

Some differences between them:

Thread

 - Thread is an operating-system-level concept.  - Thread in Java is "user thread", but it is mapped to "kernel thread" under the hood.(Non Green Thread)  - Operating system is responsible for the switching and scheduling between threads  - Threads are preemptive, memory resources can be shared between them, processes don't.  - Thread sharing resources causes Thread Synchronization Problem  - Green Threads in Java 1.1, is "user-level threads". They are scheduled by user-level process, not by the kernel.

Some people compare threads to lightweight processes.  

Coroutines

   - Kotlin Coroutines kind like "Green Threads" above. Thousands of Coroutines can run on only one thread.  - Kotlin Coroutines is not operating-system-level concept.  - Kotlin Coroutines is user-level concept, kernel know nothing about Coroutines.  - Kotlin Coroutines are not preemptive, so it's more efficient.  - Kotlin Coroutines use state machine under the hood, several Coroutines can share the same instance of state machine, so it is lightweight   Some people compare Coroutines to lightweight threads.

Thousands of Coroutines can run on only one thread.

Coroutines.005

From the perspective of the containment relationship, the relationship between coroutines and thread is a bit like the "relationship between the thread and the process".

Coroutines cannot run without threads.

Coroutines.006

Although the Coroutines cannot run without a thread, it can switch between different threads.

Coroutines-dispatch

Knowing coroutines is efficient and lightweight, but are we going to use it based on its "efficient" and "lightweight"? Assembly Language is also very efficient. C Language can also be lightweight.

Efficient and lightweight are not the core competitiveness of Kotlin Coroutines.

The killing feature of Kotlin Coroutines is: it can simplify asynchronous concurrency programming —— writing asynchronous program in sequential way.

We all know how dangerous threads concurrency is, and how difficult to maintain the code.

3. Asynchronous & Callback Hell

Taking a asynchronous Java code as an example, we send an request to query the user's information using CallBack:

https://gist.github.com/e06aaeda56cbaefae305309893a8edfa

So far so good. What if our business logic becomes like this?   Query user info --> Query friend list of that user --> Query feed list of his friends?

Code may like this:

https://gist.github.com/b152da2cacef525de59f799a7357ce5d

Crazy, right?

This is only the case of onSuccess, in the real world situation, that could be more complicated: exceptions, retries, thread scheduling, synchronization...

4. Hell to heaven: Coroutines

  We are talking about Coroutines, so what does it looks like if we re-write the code using Kotlin Coroutines?

https://gist.github.com/fe38f3c659b8b831e66cacdef1709590

Extremely simple three lines of code, right?

This is why we love Kotlin Coroutines: writing asynchronous program in sequential way.

4-1 How to use Coroutines

The reason why the code above can be written in a sequential way is because of the definition of "the three requesting functions". They are not ordinary functions, they all have modifier: suspend, which means that they are all: Suspending Functions.

https://gist.github.com/6ce470e8100511dca57ddedf6c985b66

So, what a Suspending Function really is?

4-2 Suspending Function

A suspending function is simply a function that can be paused and resumed at a later time.

Let's take a look at the execution flow of the suspending function, and notice the flashing in the animation, which means requesting server.

Coroutines-flow

From the animation above, we can learn a lot of information:

  • Thread switching is happening in the sequential code above.
  • One line of code, switching between two threads.
  • left side of =: Main Thread
  • right side of =: IO Thread
  • Every time from Main thread to, Suspend happened.
  • Every time from IO thread to Main thread, Resume happened.
  • Suspend and Resume are "unique ability" of suspending function, ordinary functions don't.
  • Suspend, just means passing execution flow to other threads, the main thread is not blocked.
  • If we run this code on Android, ANR won't happen.

Enough explanation, so how Kotlin Coroutines can Switching Between Two Threads In One Line of Code?

All the magic is hidden behind the suspend keyword in the Suspending Function.

5. Suspend under the hood

The essence of suspend is CallBack.

https://gist.github.com/ba280eac546a58d94d294318bf9c61f3

You may ask: Where is the "CallBack"? Yes, we didn't write anything about CallBack, but we wrote suspend. When Kotlin compiler detects the suspend, it will automatically convert the suspend function into a function with CallBack.

If we decompile the above suspend function into Java, the result will like this:

https://gist.github.com/41a489cf3450d2e38352bcdf0d9ea4ec

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

https://gist.github.com/eacec04ae3ecd57b7bbed29a2eb8dbb3

Let's take a look at CallBack:

https://gist.github.com/b9fbf1770c7415dc0ef6b331f4ce5b2b

As we can see, Continuation is actually a CallBack with generic parameters, and with a CoroutineContext, which is the context of the Coroutines.

The process above from Suspending Function to CallBack Function is called: Continuation-Passing-Style Transformation.

See, that is the reason why Kotlin uses Continuation instead of CallBack, just a better name.

The following animation demonstrates the change of the Function Signature of the Suspending Function during the CPS Transformation:

Coroutines-cps-signature

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

Function Type

In the above CPS process, function type has changed: from suspend ()->String to (Continuation)-> Any?.

This means that if you call a Kotlin suspending function getUserInfo() in Java, the type of getUserInfo() in Java will be: (Continuation)-> Object. (Receive Continuation as a parameter, return value is Object)

In this CPS process, suspend () becomes (Continuation) as we have explained before, but why does the return type of the function changed from: String to Any?

Return Type of Suspending Function

After the suspend function is converted by CPS, its return value representing: whether the Suspending Function is suspended or not.

This sounds a bit confusing: a suspend function is a function that can be suspended and resume. Can it be non-suspended? Yes, the suspension of Suspending Function could happen, or not. It depends.

Let's take a look some examples:

This is a normal suspending function

https://gist.github.com/7ccbbda0c57a89f27b929b571353da76

When getUserInfo() executes to withContext, it will return CoroutineSingletons.COROUTINE_SUSPENDED indicating that the function is suspended.

Now the question is coming. Is the following function a suspend function:

https://gist.github.com/68a69ee097dd53b07c6fda80d2737ab7

Answer: It is a suspend function.

But it is different from the general Suspending Function: when it is executed, it will not be suspended because it is a normal function.

So, if you write such code, the IDE will also warn you that suspend modifier is redundant:

屏幕快照 2020-08-20 下午2.40.23

When noSuspendFriendList() is called, it will not suspend, it will directly return String type: "no suspend". Such a suspend function, you can think of it as a fake suspend function.

The reason why return type is Any?

Because of the Suspending Function, it may return CoroutineSingletons.COROUTINE_SUSPENDED, may also return the actual result "no suspend", or even return null. In order to adapt to all possibilities, the function return type after CPS transformation It can only be Any?.

Summary

  • Functions with suspend modifier is Suspending Function.
  • Suspending Function, which may not always suspended during execution.
  • The Suspending Function can only be called in other Suspending Function.
  • When the Suspending Function contains other Suspending Functions, it will really be suspended.

The above is the details of the function signature changes during the CPS process.

However, this is not all of the CPS transformation, because we still don't know what Continuation is.

6. CPS transformation

The word Continuation, if you look up Dictionary or Wikipedia, you may be confused , so abstract.

It will be easier to understand Continuation through the examples in our article.

Continuation, simply explained, it's just what to do next.

Put it in the program, Continuation represents the code that needs to be executed when the program continues to run, code to be executed next or remaining code.

Take the above code as an example, when the program executes to getUserInfo(), its Continuation is the code in the red box below: Coroutines-continuation

Continuation is the code to be run next, the remaining unexecuted code.

After understanding Continuation, CPS will be quite easy to understand, it is actually: a transformation of passing the unexecuted code.

And CPS transformation is the process of converting the original sequential suspending code into CallBack asynchronous code. This transformation is done by the compiler behind the scenes, and we programmers don't perceive it, unless we read the Bytecode.

Coroutines-cps

Somebody may sneered: So simple and "naive"? Will the three Suspending Functions eventually become three Callbacks?

Of course not, it's still the idea of CPS, but much "smarter" than Callback.

Next, let's take a look at the decompiled code of the Suspending Functions. So much has been laid, all for the next part.

7. Bytecode decompilation

Decompilation of Bytecode into Java is easy to do with Android Studio. But, this time I will not post the decompiled code directly, because the logic of the decompiled code of Coroutines is messy and the readability is so bad. CPU may like this kind of code, but it is really not what human like.

So, in order to make it easier for everyone to understand, the code I posted next is the roughly equivalent code after I use Kotlin translation, which improves readability and omits unnecessary details.

I believe that if you can understand everything in this article, your understanding of coroutines will surpass most people.

This is the code we are about to study, the code before testCoroutine() decompilation:

https://gist.github.com/c3568dd1cf342a7df69e6cec29531c3d

After decompilation, the signature of the testCoroutine function becomes like this:

https://gist.github.com/e2e02187cfeb37e6f143b8397a8eedc0

The same for several other suspending functions:

https://gist.github.com/8f1663064310f8608a833f7bb9648541

Next, let's look at the body of testCoroutine() after decompilation, which is quite complicated and involves the calls of three suspending functions.

First of all, in the testCoroutine() function, there will be an additional subclass of ContinuationImpl, which is the core of the entire coroutine suspending function. The comments in the code are showing detail.  

https://gist.github.com/58c5733206c07bed2b4ca8d0aabfe2b7

The next step is to determine whether testCoroutine() is running for the first time. If it is running for the first time, it is necessary to create an instance of TestContinuation(subclass of ContinuationImpl).

https://gist.github.com/b72ea790d8e5fa95f59568b8e7f45c80

  • invokeSuspend() will eventually call testCoroutine() and will come to this If statement
  • If it is the first run, a TestContinuation instance will be created with completion as a parameter
  • This means wrapping the old Continuation as a new Continuation
  • If it is not the first run, directly assign completion to continuation
  • This means that continuation will only generate one instance during the entire life time, which can greatly save memory (compared to CallBack)

Next is the definition of several variables, there will be detailed comments in the code:

https://gist.github.com/abd313fb438f54d19928d81dcfec4259

Then we come to the core of our state machine. See the comments for details:

https://gist.github.com/fe245dd2c622ba4504eb9a0edcb5bf01

  • The when expression implements the coroutines state machine.
  • continuation.label is the key to state machine flow.
  • If continuation.label is changed once, the coroutine is suspended/resume once.(Only if the suspension really happened.)
  • After each coroutine resume, it will check whether an exception occurs.
  • The original code in testCoroutine() is "split" into each state in the state machine, and called separately.
  • getUserInfo(continuation), getFriendList(user, continuation), getFeedList(friendList, continuation) The three functions use the same continuation instance.
  • If a function is suspended, its return value will be: CoroutineSingletons.COROUTINE_SUSPENDED.
  • Before switching the coroutine, the state machine saves the previous results in the continuation in the form of member variables.

**Warning: The above code is an improved version of the decompiled code I wrote in Kotlin, you can go to Github to find [TestSuspend.kt](https://github.com/chaxiu/KotlinJetpackInAction/blob/master /app/src/main/java/com/boycoder/kotlinjetpackinaction/chapter/c09/TestSuspend.kt) and decompile it yourself and see its true version of the coroutine state machine. **

8. Animation demonstration

Is it a bit dizzy after reading a lot of text and codes above?

Take a look at this animation demonstration. After watching the animation demonstration, then look back the above text, you may gain more.

Coroutines-all

Is it over?

No, because the above animation only demonstrates the normal suspension of each coroutine. What if the coroutines does not really suspended? What does the code look like?

When coroutines not suspended

It is very easy to test, we can change one of the suspending functions to "fake" suspending function.

https://gist.github.com/d8f6a586a263ac11320746bdb1aeed28

What does testNoSuspend() look like after decompilation?

The answer is quite simple.

https://gist.github.com/f724e797245db0885f3402dfac483455

Structure of testNoSuspend() is the same as the previous testCoroutine(), but the function name has changed. The Kotlin compiler only recognizes the suspend keyword. Even if it is a "fake" suspending function, the Kotlin compiler will still perform CPS transformation.

How does the state machine of testNoSuspend() work?

In fact, it is easy to know that the conditions of "continuation.label = 0, 2, 3" are the same. Only when label = 1, suspendReturn == sFlag will make a difference.

Let's see the specific difference through animation:

Coroutines-no-suspend

Through the animation, we clearly see that for the "fake" suspend function, suspendReturn == sFlag will take the else branch. In the else branch, the state machine directly enters the next state.

There is only one last question left:

https://gist.github.com/958e9b42fbbd92b3b9d15e071ad53f2c

The answer is simple:

If you look at the decompiled Java code of the coroutines, you will see a lot of labels. The underlying bytecode of the state machine implements this go to next state through label.

Since Kotlin does not have a goto-like syntax, I will use “pseudocode” to represent the logic of go to next state.

https://gist.github.com/dcc5b80fbb771a0573ae564718d67931

Note: The above is "pseudocode", it is just logically equivalent to the coroutines state machine bytecode.

In order not to ruin your fun of studying coroutines, I am not going to explain the original bytecode here. I believe that if you understand my post, it will be a piece of cake to understand the real code after decompilation.

The following tip may help when you studying the real bytecode:

The real coroutines state machine is composed of nested label and switch.

The real Coroutines decompiled code looks like this:

https://gist.github.com/c5fa23e7d3015c7dee0727cb5b88905a

9. End

Suspending function is the most important thing in Kotlin Coroutines and should be understood thoroughly.

After reading this post, remember to run and debug our Demo:https://github.com/chaxiu/KotlinJetpackInAction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment