How does Continuation work in Kotlin Coroutine?

Viewed 81

I am studying about CPS. I was wondering how does it work.

Object createPost(
    Token token,
    Item item,
    Continuation<Post> const){...}
interface Continuation<in T> {
    val context: CoroutineContext
    fun resume(value: T)
    fun resumeWithException(exception: Throwable)
}

People says CPS is just callbacks and nothing more than that.

But

  1. I don't know why interface is used in here as a parameter.
  2. I don't know what does <in T> do in the Continuation interface.
  3. Continuation is a parameter but, what does it do actually inside and how is it be called under the hood?
1 Answers

End-user perspective

For the end-user the situation is relatively simple: continuation represents an execution flow that was suspended. It allows to resume the execution by invoking resume() or resumeWithException().

For example, assume we want to suspend for a second and then resume execution. We ask coroutines machinery to suspend, it provides a continuation object, we store it and at a later time we invoke resume() on it. Continuation object "knows" how to resume the execution:

suspend fun foo() {
    println("foo:1")
    val result = suspendCoroutine { cont ->
        thread {
            Thread.sleep(1000)
            cont.resume("OK")
        }
    }
    println("foo:2:$result")
}

suspendCoroutine() is one of possible ways to suspend and acquire a continuation to resume later. thread() and Thread.sleep() is just for demo purposes - usually, we should use delay() instead.

Very often we suspend to acquire some kind of data. This is why continuations support resuming with a result value. In above example we can see that the result of suspendCoroutine() is stored as result and we resume the continuation by passing "OK". This way after resuming result holds "OK". That explains <in T>.

Internals

This is much more complicated. Kotlin is executed in runtimes that don't support coroutines or suspending. For example, JVM can't wait inside a function without blocking any threads. This is simply not possible (I intentionally ignore Project Loom here). To make this possible, Kotlin compiler has to manipulate the bytecode and continuations take important part in this process.

As you noticed, every suspend function receives additional parameter of Continuation type. This object is used to control the process of resuming, it helps returning to the function caller and it holds the current coroutine context. Additionally, suspend functions return Any/Object to allow to signal their state to the caller.

Assume we have another function calling the first one:

suspend fun bar() {
    println("bar:1")
    foo()
    println("bar:2")
}

Then we invoke bar(). Bytecode of both foo() and bar() is much more complicated than you would expect by looking at above source code. This is what's happening:

  1. bar() is invoked with a continuation of its caller (let's ignore for now what does that mean).
  2. bar() checks if it "owns" the passed continuation. It sees not, so it assumes this is a continuation of its caller and that this is the initial execution of bar().
  3. bar() creates its own continuation object and stores the caller's continuation inside it.
  4. bar() starts executing as normal and gets to foo() point.
  5. It stores the local state, so the code offset, values of local variables, etc. in its continuation.
  6. bar() invokes foo() passing its continuation.
  7. foo() checks if it owns the passed continuation. It doesn't, continuation is owned by bar(), so foo() creates its own continuation, stores bar()'s continuation in it and starts a normal execution.
  8. Execution gets to suspendCoroutine() and similarly as earlier, the local state is stored inside foo()'s continuation.
  9. Continuation of foo() is provided to the end-user inside the lambda passed to suspendCoroutine().
  10. Now, foo() wants to suspend its execution, so it... returns... Yes, as said earlier, waiting without blocking the thread is not possible, so the only way to free the thread is by returning from the function.
  11. foo() returns with a special value that says: "execution was suspended".
  12. bar() reads this special value and also suspends, so also immediately returns.
  13. The whole call stack folds and the thread is free to go do something else.
  14. 1 second passes and we invoke cont.resume().
  15. Continuation of foo() knows how to resume the execution from the suspendCoroutine() point.
  16. Continuation invokes foo() function passing itself as a parameter.
  17. foo() checks if it owns the passed continuation - this time it does, so it assumes this is not an initial call to foo(), but it is a request to resume execution. It reads the stored state from the continuation, it loads local variables and jumps to the proper code offset.
  18. Execution progresses normally until it gets to the point where it needs to return from foo() to bar().
  19. foo() knows that this time it was not invoked by bar(), so simply returning won't work. But it still keeps a continuation of its caller, so bar() suspended at exactly the point where foo() needs to return.
  20. foo() returns with magic value that says: "resume the continuation of my caller".
  21. Continuation of bar() is resumed from the point where it executed foo().
  22. Process continues.

As you can see, this is pretty complicated. Normally, users of coroutines should not need to understand how they work internally.

Additional important notes:

  • If foo() would not suspend, it would return normally to bar() and bar() would continue execution as usual. This is to decrease the overhead of the whole process in the case suspending is not needed.
  • When resuming, continuations don't invoke their functions directly, but they ask the dispatcher to do it. Dispatcher is stored inside CoroutineContext, so also inside the continuation.
  • Note that because continuations keep a reference to the caller's continuation, they form a chain of continuations. This can be used to produce the stack trace as the real call stack has been lost when suspending.
Related