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
.