Coroutines is the most amazing feature in Kotlin.
1. Preface
In this post, I’ll introduce Kotlin Coroutines in the form of Diagram + Animation
. After reading this post, you may find out that Coroutines is not that difficult.
2. Preparation
- Clone the demo, and open it in Android Studio: https://github.com/chaxiu/KotlinJetpackInAction
3. Thread & Coroutines
Coroutines are like lightweight threads.
Thousands of Coroutines can run on only one thread.
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 be dispatched between different threads.
Knowing coroutines is efficient and lightweight, but are we going to use it based on "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 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.
4. Asynchronous & Callback Hell
Taking an asynchronous Java code as an example, we send a request to query the user’s information using CallBack:
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:
Crazy, right?
This is only the case of onSuccess, in the real world situation, it 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?
Extremely simple three lines of code, right?
This is why we love Kotlin Coroutines: writing asynchronous program in sequential way
.
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
.
So, what a Suspending Function really is?
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 note that the flashing in the animation, which means requesting a server.
From the animation above, we can learn a lot of information:
- Thread dispatching is happening in the sequential code above.
- One line of code, running on two threads.
- left side of
=
: running Main thread - right side of
=
: running IO thread - Every time from
Main thread
toIO thread
,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.
5. Suspend under the hood
The essence of suspend
is CallBack
.
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:
Let’s take a look at the definition of Continuation in Kotlin:
And the definition of CallBack
:
As we can see, Continuation is actually a CallBack with generic parameters, also 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.
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 little 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 at some examples:
This is a normal suspending function
When getUserInfo() executes to withContext
, it will return CoroutineSingletons.COROUTINE_SUSPENDED
indicating that the function is suspended.
Now we got a problem: Is the following function a suspend function:
Answer: It is a suspend function.
But there is a difference between it and 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 take it as a fake suspend function.
The reason why return type is Any?
Because of the Suspending Function, it may return CoroutineSingletons.COROUTINE_SUSPENDED
, and 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 isSuspending Function
. - Suspending Function, which may not always be suspended during execution.
- The Suspending Function can only be called in other Suspending Function or in Coroutines Scope.
- When the Suspending Function contains other Suspending Functions, it will really be suspended.
The above are the details of the function signature changes during the CPS process.
However, this is not the end of 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 get confused.
It will be easier to understand Continuation through the examples in our article.
Simply explained, Continuation 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 knowing 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, we don't perceive it unless we read the Bytecode.
Somebody may sneer: So simple and "naive"? Will the three Suspending Functions eventually become three Callbacks?
Of course not, it uses 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
The 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 for humans. (^_^)
So, in order to make it easier for everyone to understand, the code I’ll put below 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 testCoroutine()
before decompilation:
After decompilation, the signature of the testCoroutine
function becomes like this:
The same for several other suspending functions:
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.
Please read the comments in the code for details.
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(a subclass of ContinuationImpl).
- 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 lifetime, which can greatly save memory (compared to CallBack)
Next is the definition of several variables, please read the comments in the code for details.:
Then we come to the core of our state machine. See the comments for details:
- 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/resumed 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. I’ll put the true version of the coroutine state machine later
.
8. Animation demonstration
It’s a little bit dizzy after reading tuns of text and codes, right?
Take a look at this animation below. Watch this animation demonstration, then look back, you may gain more.
Is it over?
No, because the above animation only demonstrates the normal suspension of each coroutine. What if the coroutines do not really suspend? What does the code look like?
It is very easy to test, we can change one of the suspending functions to "fake" suspending function
.
What does testNoSuspend()
look like after decompilation?
The answer is quite simple.
The structure of testNoSuspend()
is the same as the previous testCoroutine()
. 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:
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
.
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 bytecode 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:
9. End
When we look back, differences between Thread and Coroutines:
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)
- The operating system is responsible for the switching and scheduling 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 the user-level process, not by the kernel.
Coroutines
- Kotlin Coroutines kind like “Green Threads” above. Thousands of Coroutines can run on only one thread.
- Kotlin Coroutines is not an operating-system-level concept.
- Kotlin Coroutines is a user-level concept, kernel knows 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.
The suspending function is the most important concept in Kotlin Coroutines, which should be understood thoroughly.
After reading this post, remember to run and debug the Demo: https://github.com/chaxiu/KotlinJetpackInAction
Star, fork, issue are welcomed.
10. Reference
This post is inspired by lots of great posts and videos, I can’t write this post without them.
Below is the main list, not all, thanks:
https://www.youtube.com/watch?v=YrrUCSi72E8