Suspend functions under the hood

Ashish Kumar
ProAndroidDev
Published in
4 min readMar 18, 2020

--

Many of us have been using coroutines to manage asynchronous tasks which might have otherwise blocked the main thread and cause our app to freeze.

When we add the suspend modifier, we tell the compiler that this function needs to be executed inside a coroutine. We can think of a suspend function as a regular function whose execution might be suspended and resumed at some point.

But what is the compiler actually doing when we mark a function as suspend and how does this suspension actually work?

Let’s get our hands dirty and take a closer look, under the hood.

The Kotlin compiler converts the suspend function to an optimized version of callbacks using a Finite State Machine. So who all recalls the theory of computation?. Well, I don’t. And this article will definitely not need those kind of callbacks (Hope someone get’s this ). The Kotlin compiler does everything for us. Yes, you are right-the Kotlin compiler will write those callbacks for us.

Suspending functions communicate with each other with the help Continuation objects. A continuation is a generic callback interface with some extra information. If you look into the code, it looks something like this

As you can see, this interface has two main members

  1. context, which is the CoroutineContext to be used in the continuation.
  2. The function-resumeWith(), which resumes the execution of the suspend function.

Note: The parameter of the function resumeWith(), could be either the value of the computation that cause the suspension, or it could be an exception.

Coming back to our original topic of discussion. How is the compiler going to modify it and what is it gonna look like?. I would like to explain that using an example of a suspend function syncData() (I genuinely gave thought to this name)

Now, the compiler replaces the suspend modifier with an extra parameter, completion of type Continuation in the function signature, which will eventually, be used to communicate the result of the computation to the coroutine that called it, as you can see in the code below.

If you notice, the return type of the transformed function is unit, instead of Long-the expected result, which will now be routed via the parameter-continuation.

Actually the Kotlin compiler identifies when a function can suspend internally and every such suspension point is represented as a state of a Finite State Machine and these states are represented with labels by the compiler. You can imagine it to be something like this for our function

For a better understanding of the state machine, the compiler will use a When statement to implement different states.

But.., do you not see that this code is incomplete? If you notice carefully(just to make sure, you do notice), there is no way for different states to share data among themselves. I am sure you forgot- with the help of the continuation. It's actually the reason why the generic of the continuation is “Any”, instead of the return type of the original function.

The compiler internally creates a private class that not only holds the data but also calls the suspend function -in our case syncData()- recursively to resume the execution of the function. Let's see what this class looks like. I am going to call that class SyncDataStateMachine (pardon my nomenclature, yet again).

As you can see, this class extends from CoroutineImpl. But what is CoroutineImpl? Well, it's just a subtype of continuation which expects the completion object as a constructor parameter (This is the continuation object which will be used to communicate back to the function). FYI, this is the same continuation we called in the State Machine. But also, this class does one more magic. It saves the variables that were declared in the original suspend function (along with some other variables which are common for all CoroutineImpls). In our case, the class would look something like this

The important member’s of this class are

  1. The result variable, which is the result of the previous computation.
  2. Label, which keeps the state of the State Machine.
  3. The invokeSuspend function, which is used to resume the state of the StateMachine, by calling the syncData function with the information of the continuation to trigger the State Machine again.

At this point, our label will be already in the next state and the result of the previous state’s continuation will be assigned.

An instance of this class is added to syncData. The first thing it needs to know is, is it the first time this function is called? If yes- create a new instance of SyncDataStateMachine and store the completion instance received as a parameter, otherwise, if the function has resumed from one of the previous states-continue with the execution of the State Machine. Programmatically, something like this

For completion, this is what the rest of the function looks like

As you can see how a state is using the continuation result of the previous state. Also, every state checks if an error has happened while the function was suspended and in the last state, it calls resume on the continuation of the function that called it.

That’s how our teeny-tiny suspend functions undergo transformations under the hood- Thanks to the Kotlin compiler.

I hope you liked the article. Make sure you give me a decent number of claps :P and follow me if you’d like to read more.

--

--

Engineer @Amazon | Open Source Enthusiast | Mentor Google Summer of Code’19 | Views are my own