Wednesday, 9 July 2025

Kotlin Coroutines Revisited. Part 2: Suspension

Part II of this series of posts that I started last week revisiting some Kotlin coroutines concepts (that' I've managed to better understand thanks to some GPT (Claude) discussions). We'll deal today with an essential part, the suspension mechanism. We know (and if you don't know it, probably this post is not for you) that each suspend function is transformed by the Kotlin compiler into a state machine. When we have a chain of suspend function calls (suspend functionA calls suspend functionB that calls suspend functionC...) the way these functions are linked (now that because of suspension and resumption we don't have then "linked" in a normal call stack) so that when functionC completes functionB gets resumed in the point where it got suspended/paused is by means of Continuation objects (callbacks on steroids). In JavaScript or Python async functions return a Promise or a Future, and it's those objects what indicates that a function is being "suspended". But in Kotlin where we pass continuations rather than returning "awaitables", how does a suspend function (the "leaf" function in our chain of suspend calls) ultimately communicate its caller that it's suspending? (it could suspend or it could just return a normal value sequentially depending on a conditional).

For a suspension to happen the last suspend function in the call (the one performing a low level asynchronous IO operation) has to invoke suspendCoroutine or suspendCancellableCoroutine, that in turn have will return the special marker value COROUTINE_SUSPENDED if the suspension really must take place. This COROUTINE_SUSPENDED value will be managed by the state machines created by the compiler for the different suspend functions and when a suspension must take place it will propagate through the stack (this suspension indicator is a normal return, so it propagates through the normal stack, then, when the suspend function "returns" a value asynchronously, it will propagate through the continuations chain).

suspendCoroutine and suspendCancellableCoroutine are intrinsic compiler functions. They're the only way to actually create a suspension point in the execution.

This is a very, very rough approximation (the signature is wrong and this state machine goes into a class with additional logic, it's not just a function) to how the state machine corresponding to a suspend function looks like:


// Conceptually, your userFunction becomes something like:
fun userFunction$stateMachine(continuation: Continuation, label: Int): Any? {
    when (label) {
        0 -> {
            // Initial call
            val result = networkCall(continuation.with(label = 1))
            if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            // Fall through to label 1
        }
        1 -> {
            // Resumed here - no "return" up the stack
            val networkResult = continuation.getResult()
            return processResult(networkResult)
        }
    }
}

And, how do these so special suspendCoroutine and suspendCancellableCoroutine functions work? Both functions receive a block (that expects as Continuation as parameter), and it's this block who will perform (or not) a low level asynchronous operation that once ready will call us back, meaning that in between our suspend functions will get suspended (to be resumed when we are called back). Both suspendCoroutine and suspendCancellableCoroutine end up calling suspendCoroutineUninterceptedOrReturn but suspendCoroutine passes over to the block a Continuation and suspendCancellableCoroutine passes over a CancellableContinuation. So in the end the magic happens in suspendCoroutineUninterceptedOrReturn. That's an intrinsic function, there's no Kotlin code for it. "Intrinsic compiler functions" are special functions that don't have normal implementations - instead, the compiler has built-in knowledge of what they do and generates specific bytecode for them.

I'll show now the code that the GPT (Claude) came up with during our discussions.

This is how a function (and the associated block) that invokes suspedCoroutine/suspendCancellableCoroutine looks. The first one is not performing anything asynchronous and hence there is no suspension. The second one is causing a suspension for real (it invokes an asynchronous function that will use as callback continuation.resume).


//Path 1: Immediate Completion (No Suspension)
suspend fun quickOperation(): String {
    return suspendCoroutine { cont ->
        // This completes immediately
        cont.resume("immediate result")
        // shouldSuspend() will return false
    }
}

//Path 2: Deferred Completion (Actual Suspension)
suspend fun networkCall(): String {
    return suspendCoroutine { cont ->
        // Register with event loop, but don't complete yet
        eventLoop.registerSocket(socket) { response ->
            cont.resume(response) // This happens later!
        }
        // Block ends without calling cont.resume()
        // shouldSuspend() will return true
    }
}


And this is the "Kotlin pseudocode" for suspendCoroutineUninterceptedOrReturn (as I've said, being an intrinsic there's no Kotlin source for it)


// Conceptual implementation of suspendCoroutine
suspend fun  suspendCoroutineUninterceptedOrReturn(block: (Continuation) -> Unit): T {
    val continuation = getCurrentContinuation()
    
    // Mark continuation as "not completed yet"
    continuation.markAsPending()
    
    block(continuation) // User code runs
    
    // Check if the block completed the continuation synchronously
    if (continuation.isCompleted) {
        // Fast path - no actual suspension needed
        return continuation.getResult()
    } else {
        // Slow path - we need to suspend
        return COROUTINE_SUSPENDED
    }
}

Notice that in the quickOperation() function that invokes suspendCoroutine with a block that returns immediatelly, that block is calling continuation.resume. That feels odd, cause as it's not suspending there's no need to resume a continuation, just set the continuation as completed and return normally. Well, Claude to the rescue, that's exactly what continuation.resume is doing in that particular case.

In practice, continuation.resume() is indeed doing exactly what you're thinking - it's setting the result and marking the continuation as completed, but without triggering any actual resumption logic.

So the same continuation.resume function manages 2 completely different situations:

- Synchronous completion:
Continuation is completed within the suspendCoroutine block
No actual suspension occurs
Result flows directly back through the call stack


- Asynchronous completion (typical case):
Continuation is completed later from a callback/event handler
Actual suspension and resumption occurs
Result triggers coroutine resumption on appropriate dispatcher

Claude can guess that its implementation should look more or less like this:


// Conceptual implementation
class ContinuationImpl {
    private var result: Any? = UNINITIALIZED
    private var isCompleted = false
    
    fun resume(value: T) {
        if (!isCompleted) {
            this.result = Result.success(value)
            this.isCompleted = true
            
            // Key point: if we're in synchronous context,
            // this doesn't trigger async resumption
            if (inSuspendCoroutineBlock) {
                // Just mark as completed, don't dispatch
            } else {
                // Actually dispatch to resume suspended coroutine
                dispatch()
            }
        }
    }
}

An additional point. The suspendCoroutine function (and suspendCancellableCoroutine and suspendCoroutineUninterceptedOrReturn) is marked with the suspend modifier, but indeed, is this necessary? suspendCoroutine is never going to get suspended (no state machine needed for it), depending on what the block does it will return a value or return the CoroutineSuspended marker value. The resuming will start in the continuation corresponding to the caller of suspendCoroutine. So, why is it marked with suspend? Again, Claude to the rescue:

The suspend modifier on suspendCoroutine is purely for access control and type safety - it ensures it's only called from suspend context where the compiler can properly handle the COROUTINE_SUSPENDED return value.

For closing up I'll add a link to a pretty good article on this topic.

No comments:

Post a Comment