Suspending functions, coroutines and state machines

We started this guide referring that Kotlin uses coroutines to address asynchronicy issues, however the mechanism presented in the previous section is called suspending functions. So, what is the relation between these suspending functions and coroutines? And how is it possible for a thread to leave a suspending function while in the middle of it, without a return in sight. A look into how suspending functions are compiled provides the necessary knowledge to answer these questions.

Continuation-passing style

The code generated by the Kotlin compiler for suspending functions uses a method named continuation passing style. On the normal direct style, when a function ends it returns to its call site, typically by assigning the program counter with the value saved in the stack.
On the continuation-passing style, the code to execute after a function ends is passed explicitly into the function as an extra parameter, called the continuation

For instance, consider the following pseudo-code

prev_statement
function(a,b)
next_statement
...

Using a direct passing style, the call can be represented with the following diagram, where the control flow is returned to the call site after function ends it execution.

direct style

Using the continuation-passing style, the code after the function call is encapsulated into another function, which is passed into the call to function.

continuation-passing style

In this case, the continuation, i.e. next_statement is called explicitly by function.

In Kotlin, the continuation is represented by the following interface

public interface Continuation<in T> {
    public fun resume(value: T)
    public fun resumeWithException(exception: Throwable)
    (...)
}

which we already saw in the suspendCoroutine function presented in the previous section.

The generated code for any suspend function will receive a Continuation as the last parameter. For instance, the following Kotlin function

suspend fun suspendFunctionWithDelay(a: Int, b: Int): Int

produces code with the following Java-equivalent signature.

public static final Object suspendFunctionWithDelay(int a, int b, @NotNull Continuation var2) 

By explicitly passing a reference to the continuation code, the CPS style allows the called function to better control when and how the continuation is called. For instance, it can be called synchronously when the function ends its processing, producing something very similar to what is obtained with the direct style. In this case, the complete flow happens in the same thread.

continuation-passing style

However, the called function can decide to call the continuation on a different moment in time and on a different thread, for instance by scheduling its call on a Java’s ScheduledExecutorService.

continuation-passing style

In this case, the main thread exits from function before the continuation is called, on a different thread. Again, this is only possible because function has direct access to the continuation reference and can decide how and when to call it.

State machine

When the Kotlin compiler generates code for a suspend function it doesn’t use the CPS style for all internal calls. Instead, only calls to other suspend functions need to use the CPS style. Calls to regular functions use the regular direct style since no suspension can happen on those.

In addition, the generated code is also optimized to avoid creating a continuation function, and the associated closure with captured state, for each suspend function call. Instead, the Kotlin compiler creates a state machine with each state representing a continuation, i.e., the code to be run after a call to a inner suspend function. When calling these, the same reference to the overall state machine is passed on the continuation parameter.

The following suspend function and associated diagram illustrates this process.

suspend fun suspendFunctionWithDelay3(a: Int, b: Int): Int {
    log.info("step 1")
    delay(1000)
    log.info("step 2")
    delay(2000)
    log.info("step 3")
    return a + b
}

state machine

We are now in a better position to understand the relation between suspending function and coroutines:

  • A suspending function defines a state machine.
  • A coroutine is an instance of that state machine, created as a result of a call to the associated suspending function.

Synchronous completion

In reality, things are a little more complex than described until now, because a call to a suspending function doesn’t always need to suspend and provide the result via the continuation. The Kotlin coroutine model allows a suspending function call to complete synchronously and provide its value on the return and never call the continuation. This additional behavior exists for two main reasons:

  • The computation represented by the suspending function might not need to suspend because everything that is needed for its conclusion is already available. For instance, a socket read may be able to terminate immediately if the amount of bytes requested is already available in the input buffer.

  • In this situation calling the continuation in the same thread can potentially originate a stack overflow, due to the nesting of continuation calls inside continuation calls. Just imagine a loop reading from an input socket where every read request can be fulfilled from the input buffer.

Due to this the code generated for a suspending function has to always cater for both situations. It does so by observing the object returned from the suspending call function:

  • If it is equal to the COROUTINE_SUSPEND constant, then the call did suspend and the result value will be delivered via a future call to the continuation, probably on a different thread.

asynchronous completion

  • Otherwise, the returned value already contains the result. The continuation wasn’t called and will not be called by the suspending function (to avoid potential stack overflows), so the flow in the calling site must continue into the continuation code.

synchronous completion

Fortunately, all this is taken care by the compiler generated code and no special care is need on the Kotlin source code.