Coroutines make our asynchronous code look sequential. Coroutines help us escape the callback hell. Coroutines save us from the complex reactive chains.
All good, but have you ever wondered what sort of sorcery happens behind the scenes? How a particular piece of code can actually perform operations asynchronously under the hood and yet give us a feel as if everything is happening sequentially? Is it magic? (Of course, it is, but in some other parallel universe) For us muggles, it all revolves around state machines, suspension points, and continuation passing.
If it is the first time you are hearing all these terms, then do not worry, this article will help you understand them. We will talk about suspension points, CPS (Continuation Passing Style), and state machines. If you are already aware of these terms, then I hope you still enjoy going through the code example and various concepts mentioned in this article.
We will first have a look at a small code snippet implemented using coroutines. Then, we will try to identify the suspension points within the code-snippet. We will then see how the code actually gets compiled into a state machine by extracting various states using suspension points. Finally, we will understand how a
Continuation implemented as a state machine is passed around to achieve something magical.
We need to populate the profile screen of a user with user details. In order to do that, we need to make two API calls. First API call we make is to
fetchUser(), after we get our
User, we then hit another API to fetch user
Profile. Using the information we have, we keep populating the UI and finally, we create an
Account object from
profile to persist.
Let us look at the pseudo-code for the API calls:
We are using
async coroutine builder in the above snippet to fetch
Profile from the API.
async coroutine builder returns a
Deferred<T> coroutine so that we can call
.await() on it to get the result at some later point.
In our example,
fetchUser() function returns a
Deferred<User> object of the coroutine that fetches
User from the API. We can then call
await() on the
Deferred coroutine to get the
fetchProfile(userId) function returns a
Deferred<Profile>, on which we can later call
await() to get the
Let us next look at the coroutine which will “sequentially” execute the above functions:
Line 1: We first set loader to visible to show that we are fetching data.
Line 2: We then call,
fetchUser() which returns a
Deferred<User> and we also immediately call
.await() on it.
await() is a
suspend function and thus, we see a suspend function indicator in the left gutter of the IDE as shown in line 2 of Snippet 2. Our coroutine will suspend at this point until the
User is returned.
So, this is the first suspension point in our code, this is where the execution of the coroutine can be suspended and then resumed later on after the async coroutine finishes and the result, i.e.
User is returned.
Let us talk about some terms while our
user is getting fetched from network.
Suspension Points and CPS (Continuation Passing Style)
suspend function is transformed by the compiler to take a
Continuation object. In the above case,
.await() suspend function will be transformed to take a
Continuation object when compiled, i.e
.await(continuation). So, we save all the state (like local variables, etc.) within this
Continuation object and pass it to the
.await(continuation) suspend function. Similarly, all other suspend functions and blocks get transformed to take a
Continuation object as well.
Continuation, as the name suggests encapsulates the current state and also the information about how to continue from a particular suspension point later on.
A suspension point in the code is like a check-point, at which the current state (like local variables, etc.) can be saved (inside a
Continuation) to be resumed later on from where we left off. Everything that follows a particular suspension point (lines of code after the suspension point) is also saved within the
Continuation object and passed on to the compiled
suspend function for later execution. Suspension points are created by using suspend functions like
So, all the suspend functions, upon transformation, get a
Continuation object passed as a parameter to them in order to resume execution later on. This is referred to as CPS (Continuation Passing Style). This pattern is similar to passing a callback and getting notified when the job is done. The difference is just that, with coroutines, these “callbacks” (or continuations to be precise) are handled for us by the compiler and we get the look and feel of sequential code execution.
Let us now resume our code-snippet, from the last suspension point 😉, i.e Line 2:
Line 2: Now, when
User is fetched from API, execution resumes right after our first suspension-point
Line 3: We can then set
user.name on the UI.
Line 4: Next, we have
user.id which we can use to fetch the
Profile of user from the API. So, we call
fetchProfile(user.id) and again immediately call
.await() on it. This is when we reach our second suspension point due to the
.await() suspend function. Execution again suspends at this point and resumes later on when
Profile is fetched from the API.
Line 5 to 8: Once, the user profile is available and returned from the
.await() function, we can then set the required information on the UI and create an
Account object to persist the session and finally, we hide our
progress indicator once we are done.
Suspension points to states
Now that we have identified, two suspension points in this code, let us have a look at how these suspension points get transformed into different states using labels.
Everything above and including a suspension-point is transformed into a state by the compiler. Everything below the suspension point until the next suspension point or till the end of the suspend function gets transformed into another state.
So, we will have three states in the above Snippet 3, i.e
- L0: until the first suspension point
- L1: until second suspension point
- L2: until the end
Generated State Machine
Now, using these three states as labels, a
Continuation state-machine is generated, which can resume execution from any of the states. Here is how the
Continuation interface looks like :
Now, let us have a look at the pseudo-code from the generated
Continuation state machine implementation by the compiler :
Each state is generated as a label and the execution can
goto a particular label when resumed. Generated
Continuation state machine implementation contains a field holding the current state, i.e.
label, and all the intermediate data, i.e. local variables shared between different states like
v2: Profile are also stored as fields.
Execution of the State Machine
State L0: When the coroutine is started,
resumeWith() is called with
label = 0, and we enter the L0 state as shown in the figure below :
We then do some work like showing a progress indicator and set the
1 before calling
.await(), which suspends the coroutine. When a coroutine is suspended,
.await() function returns the result called
COROUTINE_SUSPENDED. At this point, we pass this state machine to the
.await(continuation) function, to be able to resume state later from where we left off and the coroutine goes in
State L1: Once
User is fetched from the API, we will then call
continuation.resumeWith(user), label=1 on our saved
continuation object and proceed directly to state L1. At this point, we have fetched
User, which we can use to populate the UI, etc. We then set the
2 and suspend execution on
profile.await(continuation) while waiting for user profile and again passing in the
continuation object to resume control later on.
State L2: Finally, when
Profile is fetched, we again call
continuation.resumeWith(profile), label=2 on the saved
continuation and get to the state L2. At this point, we have fetched both
profile. We can then create an
Account object using
profile, set the data fetched on the UI, hide loader, persist the session, dance a little, etc. and finally set the
-1, indicating the end of the coroutine.
So, we have seen how suspension points help in determining various states of the state machine. This state machine then allows us to resume execution from various suspension points.
The code appears to us as sequential and behind the scenes it utilizes state machines to suspend and resume execution at various suspension points when needed.
Single suspension point
The earlier example had multiple suspension points within the coroutine. If a suspend function calls another suspend function only at the end, i.e the tail position, in such simple cases of suspend functions containing a single suspension point only and that too at the tail position, then there is no need of generating a state machine, since there is only a single state involved. Such suspend functions are compiled to just take a
Continuation parameter for suspending and resuming execution.
This brings us to the end of this article. Please note that, it is not mandatory to know about such implementation details. But I believe it definitely helps in creating a better mental model of various problems and better utilize the tools at hand.
Further Reading :
The suspend modifier — Under the hood by Manuel Vivo
Kotlin coroutines proposal document : I found that this official document helps in understanding a lot of concepts.
Special thanks to Manuel Vivo for the review :)
Kindly leave some claps if you learned something and share it with your friends. If you find an improvement, kindly leave a comment so that we can all learn together :)