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.
- Keep your Android Studio updated.
- Clone our demo, and open it in Android Studio: https://github.com/chaxiu/KotlinJetpackInAction
- Checkout the branch:
chapter_09_Coroutines_suspend.
- Run and debug while you're reading.
Some differences between them:
- 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.
- 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.
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.
Although the Coroutines cannot run without a thread, it can switch between different threads.
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.
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...
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
.
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?
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.
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
toMain 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.
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:
This transformation looks simple, but there are some details hidden in there.
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?
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
:
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.
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?
.
- Functions with
suspend
modifier isSuspending 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.
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:
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.
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.
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, andcalled 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
. **
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.
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?
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:
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
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