From 5fd08b3ee5bc0bde2d5de2f43bea619e259ccf73 Mon Sep 17 00:00:00 2001 From: Ilmir Usmanov Date: Thu, 13 Aug 2020 20:24:25 +0200 Subject: [PATCH] Document coroutines codegen: simple lambda example --- .../codegen/coroutines/coroutines-codegen.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md index 59f7aa73c28..bd5bb5d004d 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md @@ -31,3 +31,151 @@ may vary from version to version, from back-end to back-end, and one should use Lastly, most of the examples presented in the document actually suspend, so one is sure every piece is in place since coroutines is a broad and complex topic, and it is easy to forget one piece, which will lead to a runtime error or even worse, semantically wrong code execution. +## Suspend Lambda +Let us begin by introducing suspend lambdas. + +Suspend lambda is an example of a coroutine, and the compiler turns ordinary, sequential code inside the lambda into suspendable. +The example shows a simple suspend lambda: +```kotlin +suspend fun dummy() {} + +suspend fun main() { + val lambda: suspend () -> Unit = { + dummy() + println(1) + dummy() + println(2) + } + lambda() +} +``` +which, upon running, will print `1` and `2`, as expected. + +One can call a suspend function only from other suspend function or suspend lambda, but it can call ordinary, non-suspendable functions. For example, both `dummy` and `println` are used only inside the lambda. Because one is not allowed to call suspendable functions from ordinary, we can imagine two worlds: suspendable and ordinary. Alternatively, one can consider them as being of two different colors, and we color the program by using the "suspend" modifier. + +The lambda, creatively named `lambda`, contains two suspend calls (`dummy`) and one from the `main` function to the lambda itself, +but there is no suspension. Let us add it: +```kotlin +import kotlin.coroutines.* + +var c: Continuation? = null + +suspend fun suspendMe() = suspendCoroutine { continuation -> + println("Suspended") + c = continuation +} + +suspend fun main() { + val lambda: suspend () -> Unit = { + suspendMe() + println(1) + suspendMe() + println(2) + } + lambda() +} +``` +Now, when we run the code, it prints `Suspended` and nothing else; it does not even finish the execution of the program, since `lambda` is, in fact, suspended, and it suspends `suspend fun main` as well. + +To fix the issue with the suspension of `main`, we need to cross a boundary between suspendable and ordinary worlds and make +`main` ordinary, so, when it starts a coroutine, and the coroutine suspends, `main` does not. Since one cannot call a suspendable +function from an ordinary one, there are special functions, so-called coroutine builders, whose sole purpose is to create a coroutine, run it, and when it suspends, return execution to the caller. +Other than that, they act like other ordinary functions. +Let's name ours, I don't know, `builder`: + +```kotlin +fun builder(c: suspend () -> Unit) { + c.startCoroutine(object: Continuation { + override val context = EmptyCoroutineContext + override fun resumeWith(result: Result) { + result.getOrThrow() + } + }) +} +``` +A separate section explains the exact mechanism of starting a coroutine (in a broad sense) and how one can write their builders. For now, consider `builder` as a boilerplate to cross the worlds. + +Now, when we change `main` to use the builder and not suspend itself +```kotlin +fun main() { + val lambda: suspend () -> Unit = { + suspendMe() + println(1) + suspendMe() + println(2) + } + builder { + lambda() + } +} +``` +and then run the example, it will print expected `Suspended`, but this time it will exit the program. + +Additionally, when we change `main` to resume the lambda +```kotlin +import kotlin.coroutines.* + +var c: Continuation? = null + +suspend fun suspendMe() = suspendCoroutine { continuation -> + println("Suspended") + c = continuation +} + +fun builder(c: suspend () -> Unit) { + c.startCoroutine(object: Continuation { + override val context = EmptyCoroutineContext + override fun resumeWith(result: Result) { + result.getOrThrow() + } + }) +} + +fun main() { + val lambda: suspend () -> Unit = { + suspendMe() + println(1) + suspendMe() + println(2) + } + builder { + lambda() + } + c?.resume(Unit) +} +``` +it will print +```text +Suspended +1 +Suspended +``` +`lambda` is resumed and then suspended once more. If we add a couple more `c?.resume(Unit)` +```kotlin +fun main() { + val lambda: suspend () -> Unit = { + suspendMe() + println(1) + suspendMe() + println(2) + } + builder { + lambda() + } + c?.resume(Unit) + c?.resume(Unit) + c?.resume(Unit) +} +``` +we will get +```text +Suspended +1 +Suspended +2 +Exception in thread "main" java.lang.IllegalStateException: Already resumed +``` + +The last line is what we get when we try to resume a finished continuation. + +In this little example happens a lot. The rest of the section explains it bit by bit, starting with a state-machine.