Coroutines: Suspending State Machines

Garima Jain
Google Developer Experts
8 min readJun 3, 2020

--

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.

Problem Statement:

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 user and profile to persist.

Let us look at the pseudo-code for the API calls:

Snippet 1: Fetching User and Profile using async coroutine builders

We are using async coroutine builder in the above snippet to fetch User and 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 User result.

Similarly, fetchProfile(userId) function returns a Deferred<Profile>, on which we can later call await() to get the Profile of User.

Let us next look at the coroutine which will “sequentially” execute the above functions:

Snippet 2: fetch User and Profile using coroutines

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)

Every 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.

All suspend functions are compiled to take Continuation implementation

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 theContinuation object and passed on to the compiledsuspend function for later execution. Suspension points are created by using suspend functions like delay,yield, suspendCancellableCoroutine, etc.

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:

Snippet 2: fetch User and Profile using coroutines

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.

Snippet 3: Identifying states for the state machine

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 :

Continuation interface

Now, let us have a look at the pseudo-code from the generated Continuation state machine implementation by the compiler :

Snippet 4: Continuation state-machine implementation pseudo-code

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 v1: User, 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 :

State Machine Diagram for fetching User Profile

We then do some work like showing a progress indicator and set the label to 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 SUSPENDED state.

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 label to 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 user and profile. We can then create an Account object using user and profile, set the data fetched on the UI, hide loader, persist the session, dance a little, etc. and finally set the label to -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 :)

--

--