Skip to content

Kotlin Coroutines: The Suspend Function

  • by

Welcome to my first blog post! I'd like to start it off with a topic that I spend a lot of my professional time working with/thinking about: Kotlin Coroutines! Coroutines aren't a new concept in programming. In short, coroutines allow a function to suspend and resume. The ability to suspend a function has a lot of use cases. In Kotlin, it's most used for performing asynchronous computations. To suspend in Kotlin Coroutines, you'd simply add the suspend modifier to your function. Just what exactly does adding this suspend modifier do? How does it work behind the scenes? That's what we'll be exploring in this blog.

First let's see a simple example of a suspending function. The following suspend function just waits for 1 second:

suspend fun foo() {
     // let's assume called on main thread
     delay(1000) // resume in 1 second
     // we resumed after 1 second. which thread? that'll be answered in a future blog
     println("foo completed")
 }

Here we waited for one second but note: we're not blocking whatever thread it's being called on (let's just say main thread for this example). This is all asynchronous. We simply suspended execution of the function, and then 1 second later control of the function is given back. What's even more interesting is that there's no callback involved here.

How are we waiting asynchronously for some amount of time, yet our code is in a synchronous style?. This may seem like magic, but in reality, suspend functions just hide what's actually happening at the Java Bytecode level. We write suspend in our signature, but the code is actually transformed without us knowing. Let's break it down.

Suspend function signature transformation

Here's what the previous foo function signature actually looks like after transformation:

fun foo(continuation: Continuation<Unit>): Any?

What's happening here? A new parameter was added and the function now returns a nullable Any. The reason we have an Any? return type is because suspend functions may not actually suspend. They have the suspend modifier on the signature, but in some cases they may just return without ever suspending or throw an exception (before any suspend). When a suspend function like foo actually suspends, the actual value of the Any? will be a special singleton object: COROUTINE_SUSPENDED. Otherwise it just returns the result.

Now for the Continuation parameter. It is an interface in the kotlinx.coroutines package:

interface Continuation<in T> {
   val context: CoroutineContext // for a later blog :)
   fun resumeWith(result: Result<T>)
}

We'll get more into CoroutineContext in another blog, but essentially the Continuation interface just represents a callback (with some state from CoroutineContext). That's right: coroutines is just writing callbacks for you!

We explained how the signature of a suspend function is transformed, but what does the implementation of suspend functions look like?

Suspend function state machine

I mentioned before that suspend functions are essentially writing callbacks for you. You may think that under the hood there's just this auto generated callback hell happening. But that's not actually the case. Having a bunch of nested callbacks means that you'd have to allocate function objects. And of course that's not very efficient. Instead, the implementation of suspend functions are done with a state machine.

Each suspend function allocates a single state machine object where each suspension point is a state. Also, any local variables in between any suspension points are stored on the state machine. Confusing? Let's look at an example of how this state machine would look like.

First a more complicated suspend function example:

suspend fun performNetworkequest(){
    val response = getResponse('https://blah.com') //suspension point
    storeResponseInDatabase(response) //suspension point
}

Here's what the function would look like after being transformed by the compiler:

fun performNetworkRequest(cont: Continuation<Unit>): Any? {
    val stateMachine = object: ContinuationImpl<Unit> {
        var label = 0
        override fun resumeWith(...) {
            performNetworkRequest(this) // call with this state machine again
        }
    }

    var response: Response? = null
    switch(stateMachine.label) {
        case 0:
            stateMachine.label = 1 // set next state of state machine
            response = getResponse('https://blah.com', stateMachine)
            break
        case 1:
            stateMachine.label = 2 // set next state of state machine
            storeResponseInDatabase(response, stateMachine)
            return Unit;
    }
}

Of course my post-compiler example is not 100% accurate, but it's pretty close. Again as I said, each suspension point is assigned a "state" (the label + any local variables if applicable). The one main object allocated was the state machine. That's what makes suspend function very efficient vs generating a bunch of callbacks!

Conclusion

And that's it! I hope you learned a lot about how suspend functions work underneath the hood! I plan to do multiple blogs that take deep dives into other aspects of Kotlin Coroutines. Next I'll take a look at CoroutineContext.

Leave a Reply

Your email address will not be published. Required fields are marked *