https://kotlinlang.org logo
#coroutines
Title
# coroutines
f

frankelot

08/27/2021, 7:50 AM
I'm trying to understand coroutine internals by watching this

excelent video

by Roman Elizarov. Unfortunately, There's something that doesn't add up for me. According to the slides, each
suspending
function is transformed into a regular function that takes an additional parameter
Continuation<OriginalReturnType>
... (emphasis in
OriginalReturnType
) But then in the second screenshot, the same
Continuation
seems to be used to call multiple suspending functions which different return types (
requestToken
and
createPost
) I would think
suspend fun requestToken
is transformed to
requestToken(Continuation<Token>)
and that
suspend fun createPost
should be
createPost(Continution<Post>)
right? So how come we can pass
sm
into both?
l

louiscad

08/27/2021, 8:14 AM
There's type erasure for generics, so the same continuation can actually be used across any suspend call in the chain.
f

frankelot

08/27/2021, 8:19 AM
Right! That explains it... It is still not clear to me how the "parent/child" relationship works for suspending functions state machines
The first time a
suspending
function gets called, it gets the "parent" state machine continuation... but also, this suspending function creates its own (to pass it down to it's children)
is the parent continuation stored inside the child continuation? 🤔 I'm trying to read decompiled bytecode to understand it, but I still don't get it
l

louiscad

08/27/2021, 8:22 AM
I think the parent continuation resumes when the child one reaches a terminal step (like what is initially a return expression or last function line)
f

frankelot

08/27/2021, 8:24 AM
Im actually surprised to see
return
statements in the decompiled code, I thought they would be replaced by
continuation.resumeWith(...)
instead
resumeWith
is nowhere to be seen
I guess I need to read into this a bit more, I can't even formulate the proper questions at this time 😄
l

louiscad

08/27/2021, 8:31 AM
Note that the actual implementation evolved a bit in Kotlin 1.3, when coroutines dropped their experimental status, which came after this talk.
f

frankelot

08/27/2021, 8:37 AM
Thanks! Do you happen to know of any other video/article about this that might help? Or how did you go about getting your head around all this?
l

louiscad

08/27/2021, 8:39 AM
I don't mind that too much actually. I know roughly how it works, and it's enough. Actually, it's not even necessary to successfully leverage coroutines. But I watched that talk and then looked at how it went from 2 functions to 1 with the
Result
inline class. You might be interested in the coroutines KEEP document (I also read it a few years ago).
f

frankelot

08/27/2021, 8:42 AM
Thanks again! I will be giving that a read 👀
I think I'm at a point where I can use coroutines comfortably but I'm preparing a talk and I'd like to get into the details of how they work... this made me realise that maybe I don't really know what's going on under the hood, I just have a rough "intuition"
r

raulraja

08/27/2021, 2:14 PM
@frankelot I think the part you are looking for may be https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#state-machines . You can also place a small program with suspend functions and compile it. There you’ll see with javap or
decompile java
with IDEA the codegen the compiler is generating. Suspend function application is desugared into a state machine loop with labels where COROUTINE_SUSPENDED is also handled. Here is a simple decompiled program that calls two suspend functions in main. foo2 actually suspends the coroutine and resumes it https://gist.github.com/raulraja/3f339c00ed4b5e25330f4e80700bf91b
f

frankelot

08/27/2021, 2:18 PM
Hey Raul! thanks for chipping in, that's exactly what I'm doing. I think I'm closer to understanding this but there's a few details still don't make sense to me, I'm not great at reading decompiled code
I wrote this
Copy code
suspend fun Bar(): Int {
        delay(100)
        return 1
    }
and decompiled...
Copy code
public final Object Bar(@NotNull Continuation var1) {
   Object $continuation;
   label20: {
      // ... ommited code ...

      $continuation = new ContinuationImpl(var1) {
         // $FF: synthetic field
         Object result;
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            this.result = $result;
            this.label |= Integer.MIN_VALUE;
            return Foo.this.Bar(this);
         }
      };
   }

   ...       // ... ommited code (state machine) ... ....

   return Boxing.boxInt(1);
}
I ommited some code because it was too long to post here... it looks like the parent-child relationship is mantained because the. compiler creates a new
ContinuationImpl
and passes the parent continuation in the constructor parameter 🙂 ...
now what I'm trying to understand is.. how are the
return
statements chained between parent and child
suspending function
The return type is
Object
because it's a union between T and a singleton
SUSPENDING
.. but I don't get how that bit works
r

raulraja

08/27/2021, 2:27 PM
The return type is Object because when the functions are translated and codegen they can return actually Any value while in the generated loop including constants like COROUTINE_SUSPENDED. For example:
Copy code
suspend foo(): Unit
suspend foo2(): Unit // this one calls suspendCoroutine { resumeWith(Unit) }
https://gist.github.com/raulraja/3f339c00ed4b5e25330f4e80700bf91b#file-suspendtestkt-decompiled-java-L78-L84 Here you can see how it checks the result of the function and it may actually be equal to the constant COROUTINE_SUSPENDED
Underneath to hold the actual values it uses the Result type which is a specialized inline union of Throwable | YourType. You can see in the code how it both validates as early as it cans if there is a failure with
throwOnFailure
and also extracts values from it with
getOrThrow
f

frankelot

08/27/2021, 2:33 PM
I see, this is starting to make sense... so here it returns COROUTINE_SUSPEND to the caller... this happens all the way back to the root I think
so what happens at the root 🤔 where is this handled
I'll try decompiling a
launch { foo() }
to see what the coroutine does with this
(https://github.com/JetBrains/kotlin/pull/3709 isn't merge but the author seems to be keeping it up to date)
but that should cover all the implementation details in one place
r

raulraja

08/28/2021, 7:19 AM
that’s amazing detailed write up, thanks for sharing @ephemient
m

Matthias Geisler

08/28/2021, 9:52 AM
Wow...mindblowing...
i

Ilmir Usmanov [JB]

08/28/2021, 3:17 PM
Oh boy, let me tackle the questions: 1. So how come we can pass 
sm
 into both? As explained by @louiscad we erase type parameters of generics. 2. is the parent continuation stored inside the child continuation? Yes. Every generated continuation class has $completion field, which is a parent continuation. 3. Im actually surprised to see 
return
 statements in the decompiled code There two ways a suspend function can be called. Directly (by calling the function) and using continuation.resumeWith method. At first, all suspend functions are called directly, then they either suspend (by returning COROUTINE_SUSPENDED marker) or return. If they simply return, there is no need to call $completion.resumeWith. If they suspend, we need to resume them first, by calling continuation.resumeWith method. The method calls the suspend function, and when the function returns, it returns to continuation.resumeWith, which, in turn, calls $completion.resumeWith (remember, $completion is the parent continuation) and the cycle continues - it in turn calls its suspend function, waits for its completion, calls its $completion.resumeWith and so on until we reach root continuation, which is defined in a builder function (async, runBlocking etc). More on than in Continuation Passing Style section of the aforementioned documentation. https://github.com/JetBrains/kotlin/blob/f91688d1a338d54649bfde1fe1e71b8a5108e91b/[…]c/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md 4. how are the 
return
 statements chained between parent and child 
suspending function
See 3. 5. so what happens at the root 🤔 where is this handled Well, it reaches the builder function (async) and simply... ignored. So the execution returns to the caller of the builder. The coroutine (in a broad sense) is suspended as well as all the suspend functions, forming it. So, basically, there are two main parts of how coroutines work - suspend and resume. When a coroutine suspend (by calling suspendCoroutine and alike), it returns COROUTINE_SUSPENDED marker, which bubble through call stack, reaching a builder function, where it is ignored and the execution returns to non-coroutine code. When we resume a coroutine, we call continuation.resumeWith. It runs the rest of suspend function and then calls $completion.resumeWith. Again, all this bubbles till the builder function.
K 3
🚀 1
f

frankelot

08/29/2021, 7:13 PM
Thank you so much to everyone that chipped in 🙂 I think it clicked for me after reading the code in
BaseContinuationImpl#resumeWith
The fact that it uses a loop instead of recursion got me at first, but I guess this is just an optimisation to avoid potential
stackoverflows
(just a guess)
Thanks for the detailed explanation to my questions @Ilmir Usmanov [JB]!
6 Views