
- Suspending functions are like state machines, with a possible state at the beginning of the function and after each suspending function call.
- Both the number identifying the state and references to the local variables are stored in the continuation object before suspending functions are called.
- Continuation of a function decorates a continuation of its caller function; as a result, all these continuations represent a call stack that is used when we resume or a resumed function completes.
- When a suspending function is resumed, it first calls its function; once this is done, it resumes the continuation of the function that called this function. This continuation calls its function, and the process repeats until the top of the stack is reached.
suspend fun getUser(): User? suspend fun setUser(user: User) suspend fun checkAvailability(flight: Flight): Boolean // under the hood is fun getUser(continuation: Continuation<*>): Any? fun setUser(user: User, continuation: Continuation<*>): Any fun checkAvailability( flight: Flight, continuation: Continuation<*> ): Any
Any or Any?. Why so? The reason is that a suspending function might be suspended, and so it might not return a declared type. In such a case, it returns a special COROUTINE_SUSPENDED marker, which we will later see in practice. For now, just notice that since getUser might return User? or COROUTINE_SUSPENDED (which is of type Any), its result type must be the closest supertype of User? and Any, so it is Any?. Perhaps one day Kotlin will introduce union types, in which case we will have User? | COROUTINE_SUSPENDED instead.suspend fun myFunction() { println("Before") delay(1000) // suspending println("After") }
myFunction function signature will look under the hood:fun myFunction(continuation: Continuation<*>): Any
MyFunctionContinuation (the actual continuation is an object expression and has no name, but it will be easier to explain this way). At the beginning of its body, myFunction will wrap the continuation (the parameter) with its own continuation (MyFunctionContinuation).val continuation = MyFunctionContinuation(continuation)
val continuation = if (continuation is MyFunctionContinuation) continuation else MyFunctionContinuation(continuation)
val continuation = continuation as? MyFunctionContinuation ?: MyFunctionContinuation(continuation)
suspend fun myFunction() { println("Before") delay(1000) // suspending println("After") }
label. At the start, it is 0, therefore the function will start from the beginning. However, it is set to the next state before each suspension point so that we start from just after the suspension point after a resume.// A simplified picture of how myFunction looks under the hood fun myFunction(continuation: Continuation<Unit>): Any { val continuation = continuation as? MyFunctionContinuation ?: MyFunctionContinuation(continuation) if (continuation.label == 0) { println("Before") continuation.label = 1 if (delay(1000, continuation) == COROUTINE_SUSPENDED){ return COROUTINE_SUSPENDED } } if (continuation.label == 1) { println("After") return Unit } error("Impossible") }
delay is suspended, it returns COROUTINE_SUSPENDED, then myFunction returns COROUTINE_SUSPENDED; the same is done by the function that called it, and the function that called this function, and all other functions until the top of the call stack[^104_4]. This is how a suspension ends all these functions and leaves the thread available for other runnables (including coroutines) to be used.delay call didn't return COROUTINE_SUSPENDED? What if it just returned Unit instead (we know it won't, but let's hypothesize)? Notice that if the delay just returned Unit, we would just move to the next state, and the function would behave like any other.cont = object : ContinuationImpl(continuation) { var result: Any? = null var label = 0 override fun invokeSuspend(`$result$: Any?): Any? { this.result = $result`; return myFunction(this); } };
MyFunctionContinuation. I also decided to hide the inheritance by inlining the ContinuationImpl body. The resulting class is simplified: I've skipped many optimizations and functionalities so as to keep only what is essential.In JVM, type arguments are erased during compilation; so, for instance, bothContinuation<Unit>orContinuation<String>become justContinuation. Since everything we present here is Kotlin representation of JVM bytecode, you should not worry about these type arguments at all.
import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.resume //sampleStart fun myFunction(continuation: Continuation<Unit>): Any { val continuation = continuation as? MyFunctionContinuation ?: MyFunctionContinuation(continuation) if (continuation.label == 0) { println("Before") continuation.label = 1 if (delay(1000, continuation) == COROUTINE_SUSPENDED){ return COROUTINE_SUSPENDED } } if (continuation.label == 1) { println("After") return Unit } error("Impossible") } class MyFunctionContinuation( val completion: Continuation<Unit> ) : Continuation<Unit> { override val context: CoroutineContext get() = completion.context var label = 0 var result: Result<Any>? = null override fun resumeWith(result: Result<Unit>) { this.result = result val res = try { val r = myFunction(this) if (r == COROUTINE_SUSPENDED) return Result.success(r as Unit) } catch (e: Throwable) { Result.failure(e) } completion.resumeWith(res) } } //sampleEnd private val executor = Executors .newSingleThreadScheduledExecutor { Thread(it, "scheduler").apply { isDaemon = true } } fun delay(timeMillis: Long, continuation: Continuation<Unit>): Any { executor.schedule({ continuation.resume(Unit) }, timeMillis, TimeUnit.MILLISECONDS) return COROUTINE_SUSPENDED } fun main() { val EMPTY_CONTINUATION = object : Continuation<Unit> { override val context: CoroutineContext = EmptyCoroutineContext override fun resumeWith(result: Result<Unit>) { // This is root coroutine, we don't need anything in this example } } myFunction(EMPTY_CONTINUATION) Thread.sleep(2000) // Needed to don't let the main finish immediately. } val COROUTINE_SUSPENDED = Any()

How to show the bytecode generated from the file.

The bytecode generated from the file. Notice the "Decompile" button, which lets us decompile this bytecode to Java.

Bytecode from the Kotlin suspending function decompiled into Java.
suspend fun myFunction(surname: String) { println("Before") var name = "John" delay(1000) // suspending println("His name is: $name $surname") println("After") }
name is needed in two states (for a label equal to 0 and 1), so it needs to be kept in the continuation. It will be stored right before suspension. Restoring these kinds of properties happens at the beginning of the function. Parameter surname also needs to be stored in the continuation, but since it is a parameter, it is referenced when continuation is created, and used when the function is resumed. So, this is how the (simplified) function looks under the hood:import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.coroutines.* //sampleStart fun myFunction(surname: String, continuation: Continuation<Unit>): Any { val continuation = continuation as? MyFunctionContinuation ?: MyFunctionContinuation(surname, continuation) var name = continuation.name if (continuation.label == 0) { println("Before") name = "John" continuation.name = name continuation.label = 1 if (delay(1000, continuation) == COROUTINE_SUSPENDED){ return COROUTINE_SUSPENDED } } if (continuation.label == 1) { println("His name is: $name") println("After") return Unit } error("Impossible") } class MyFunctionContinuation( val surname: String, val completion: Continuation<Unit> ) : Continuation<Unit> { override val context: CoroutineContext get() = completion.context var result: Result<Unit>? = null var label = 0 var name: String? = null override fun resumeWith(result: Result<Unit>) { this.result = result val res = try { val r = myFunction(surname, this) if (r == COROUTINE_SUSPENDED) return Result.success(r as Unit) } catch (e: Throwable) { Result.failure(e) } completion.resumeWith(res) } } //sampleEnd private val executor = Executors.newSingleThreadScheduledExecutor { Thread(it, "scheduler").apply { isDaemon = true } } fun delay(timeMillis: Long, continuation: Continuation<Unit>): Any { executor.schedule({ continuation.resume(Unit) }, timeMillis, TimeUnit.MILLISECONDS) return COROUTINE_SUSPENDED } fun main() { val EMPTY_CONTINUATION = object : Continuation<Unit> { override val context: CoroutineContext = EmptyCoroutineContext override fun resumeWith(result: Result<Unit>) { // This is root coroutine, we don't need anything in this example } } myFunction(EMPTY_CONTINUATION) Thread.sleep(2000) // Needed to prevent main() from finishing immediately. } private val COROUTINE_SUSPENDED = Any()
Notice that continuations only reference existing objects; they do not copy them. This operation is cheap. The biggest cost of calling a suspending function is a creation of a continuation object, which is a lightweight object, but still an object.
suspend fun printUser(token: String) { println("Before") val userId = getUserId(token) // suspending println("Got userId: $userId") val userName = getUserName(userId, token) // suspending println(User(userId, userName)) println("After") }
getUserId and getUserName. We also added a parameter token, and our suspending function also returns some values. This all needs to be stored in the continuation:token, that is necessary to callgetUserName,userId, because it is necessary in states 1 and 2,resultof typeResult, which represents how this function was resumed.
Result.Success(value). In such a case, we can get and use this value. If it was resumed with an exception, the result will be Result.Failure(exception). In such a case, this exception will be thrown.import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.coroutines.* //sampleStart fun printUser( token: String, continuation: Continuation<*> ): Any { val continuation = continuation as? PrintUserContinuation ?: PrintUserContinuation( continuation as Continuation<Unit>, token ) var result: Result<Any>? = continuation.result var userId: String? = continuation.userId val userName: String if (continuation.label == 0) { println("Before") continuation.label = 1 val res = getUserId(token, continuation) if (res == COROUTINE_SUSPENDED) { return COROUTINE_SUSPENDED } result = Result.success(res) } if (continuation.label == 1) { userId = result!!.getOrThrow() as String println("Got userId: $userId") continuation.label = 2 continuation.userId = userId val res = getUserName(userId, continuation) if (res == COROUTINE_SUSPENDED) { return COROUTINE_SUSPENDED } result = Result.success(res) } if (continuation.label == 2) { userName = result!!.getOrThrow() as String println(User(userId as String, userName)) println("After") return Unit } error("Impossible") } class PrintUserContinuation( val completion: Continuation<Unit>, val token: String ) : Continuation<String> { override val context: CoroutineContext get() = completion.context var label = 0 var result: Result<Any>? = null var userId: String? = null override fun resumeWith(result: Result<String>) { this.result = result val res = try { val r = printUser(token, this) if (r == COROUTINE_SUSPENDED) return Result.success(r as Unit) } catch (e: Throwable) { Result.failure(e) } completion.resumeWith(res) } } //sampleEnd fun main() { toStart() } private val executor = Executors.newSingleThreadScheduledExecutor { Thread(it, "scheduler").apply { isDaemon = true } } data class User(val id: String, val name: String) object ApiException : Throwable("Fake API exception") fun getUserId(token: String, continuation: Continuation<String>): Any { executor.schedule({ continuation.resume("SomeId") }, 1000, TimeUnit.MILLISECONDS) return COROUTINE_SUSPENDED } fun getUserName(userId: String, continuation: Continuation<String>): Any { executor.schedule({ continuation.resume("SomeName") // continuation.resumeWithException(ApiException) }, 1000, TimeUnit.MILLISECONDS) return COROUTINE_SUSPENDED } fun toStart() { val EMPTY_CONTINUATION = object : Continuation<Unit> { override val context: CoroutineContext = EmptyCoroutineContext override fun resumeWith(result: kotlin.Result<Unit>) { if (result.isFailure) { result.exceptionOrNull()?.printStackTrace() } } } printUser("SomeToken", EMPTY_CONTINUATION) Thread.sleep(3000) // Needed to prevent the function from finishing immediately. } private fun Result<*>.throwOnFailure() { if (isFailure) throw exceptionOrNull()!! } private val COROUTINE_SUSPENDED = Any()
a calls function b, the virtual machine needs to store the state of a somewhere, as well as the address where execution should return once b is finished. All this is stored in a structure called call stack[^104_2]. The problem is that when we suspend, we free a thread; as a result, we clear our call stack. Therefore, the call stack is not useful when we resume. Instead, the continuations serve as a call stack. Each continuation keeps the state where we suspended (as a label) the function's local variables and parameters (as fields), and the reference to the continuation of the function that called this function. One continuation references another, which references another, etc. As a result, our continuation is like a huge onion: it keeps everything that is generally kept on the call stack. Take a look at the following example:suspend fun a() { val user = readUser() b() b() b() println(user) } suspend fun b() { for (i in 1..10) { c(i) } } suspend fun c(i: Int) { delay(i * 100L) println("Tick") }
CContinuation(
i = 4,
label = 1,
completion = BContinuation(
i = 4,
label = 1,
completion = AContinuation(
label = 2,
user = User@1234,
completion = ...
)
)
)
Looking at the above representation, how many times was "Tick" already printed (assumereadUseris not a suspending function)[^104_3]?
override fun resumeWith(result: Result<String>) { this.result = result val res = try { val r = printUser(token, this) if (r == COROUTINE_SUSPENDED) return Result.success(r as Unit) } catch (e: Throwable) { Result.failure(e) } completion.resumeWith(res) }
a calls function b, which calls function c, which is suspended. During resuming, the c continuation first resumes the c function. Once this function is done, the c continuation resumes the b continuation that calls the b function. Once it is done, the b continuation resumes the a continuation, which calls the a function.
resumeWith and then wrapped with Result.failure(e), and then the function that called our function is resumed with this result.
class UserRepository { suspend fun getUser(id: String): User { // UserRepository is stored in the continuation return getUserFromApi(id) } // ... }
- constructing a better exceptions stack trace;
- adding coroutine suspension interception (we will talk about this feature later);
- optimizations on different levels (like removing unused variables or tail-call optimization).
BaseContinuationImpl from Kotlin version "1.5.30"; it shows the actual resumeWith implementation (other methods and some comments skipped):internal abstract class BaseContinuationImpl( val completion: Continuation<Any?>? ) : Continuation<Any?>, CoroutineStackFrame, Serializable { // This implementation is final. This fact is used to // unroll resumeWith recursion. final override fun resumeWith(result: Result<Any?>) { // This loop unrolls recursion in // current.resumeWith(param) to make saner and // shorter stack traces on resume var current = this var param = result while (true) { // Invoke "resume" debug probe on every resumed // continuation, so that a debugging library // infrastructure can precisely track what part // of suspended call stack was already resumed probeCoroutineResumed(current) with(current) { val completion = completion!! // fail fast // when trying to resume continuation // without completion val outcome: Result<Any?> = try { val outcome = invokeSuspend(param) if (outcome === COROUTINE_SUSPENDED) return Result.success(outcome) } catch (exception: Throwable) { Result.failure(exception) } releaseIntercepted() // this state machine instance is terminating if (completion is BaseContinuationImpl) { // unrolling recursion via loop current = completion param = outcome } else { // top-level completion reached -- // invoke and return completion.resumeWith(outcome) return } } } } // ... }
- Suspending functions are like state machines, with a possible state at the beginning of the function and after each suspending function call.
- Both the number identifying the state and references to the local variables are stored in the continuation object before suspending functions are called.
- Continuation of a function decorates a continuation of its caller function; as a result, all these continuations represent a call stack that is used when we resume or a resumed function completes.
- When a suspending function is resumed, it first calls its function; once this is done, it resumes the continuation of the function that called this function. This continuation calls its function, and the process repeats until the top of the stack is reached.
[^104_2]: The call stack has limited space. When it has all been used,
StackOverflowError occurs. Does this remind you of some popular website we use to ask or answer technical questions?[^104_3]: The answer is 13. Since the label on
AContinuation is 2, one b function call has already finished (this means 10 ticks). Since i equals 4, three ticks have already been printed in this b function.[^104_4]: More concretely,
COROUTINE_SUSPENDED is propagated until it reaches either the builder function or the 'resume' function.[^104_5]: A local variable that is defined and used in the same state does not need to be stored in the continuation, so it is not stored.