[go: up one dir, main page]

0% found this document useful (0 votes)
12 views10 pages

Coroutines 5

Uploaded by

Alex Trujillo
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views10 pages

Coroutines 5

Uploaded by

Alex Trujillo
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

How does suspension work?

36

suspend fun requestNews(): News {


return suspendCancellableCoroutine<News> { cont ->
requestNews(
onSuccess = { news -> cont.resume(news) },
onError = { e -> cont.resumeWithException(e) }
)
}
}

Suspending a coroutine, not a function

One thing that needs to be stressed here is that we suspend a corou-


tine, not a function. Suspending functions are not coroutines, just
functions that can suspend a coroutine¹². Imagine that we store a
function in some variable and try to resume it after the function call.

// Do not do this
var continuation: Continuation<Unit>? = null

suspend fun suspendAndSetContinuation() {


suspendCoroutine<Unit> { cont ->
continuation = cont
}
}

suspend fun main() {


println("Before")

suspendAndSetContinuation()
continuation?.resume(Unit)

println("After")
}
// Before

This makes no sense. It is equivalent to stopping a game and planning


to resume it at a later point in the game. resume will never be called.
¹²Suspending main function is a special case. Kotlin compiler
starts it in a coroutine.
How does suspension work? 37

You will only see “Before”, and your program will never end unless
we resume on another thread or another coroutine. To show this, we
can set another coroutine to resume after a second.

// Do not do this, potential memory leak


var continuation: Continuation<Unit>? = null

suspend fun suspendAndSetContinuation() {


suspendCoroutine<Unit> { cont ->
continuation = cont
}
}

suspend fun main() = coroutineScope {


println("Before")

launch {
delay(1000)
continuation?.resume(Unit)
}

suspendAndSetContinuation()
println("After")
}
// Before
// (1 second delay)
// After

Summary

I hope now you have a clear picture of how suspension works from
the user’s point of view. It is important, and we will see it throughout
the book. It is also practical, as now you can take callback functions
and make them suspending functions. If you are like me and like to
know exactly how things work, you are likely still wondering about
how it is implemented. If you’re curious about this, it will be covered
in the next chapter. If you don’t feel you need to know, just skip it. It
is not very practical, it just reveals the magic of Kotlin coroutines.
Coroutines under the hood 38

Coroutines under the hood

There is a certain kind of person who cannot accept that a car can just
be driven. They need to open its hood to understand how it works. I
am one of these people, so I just had to find out how coroutines work.
If you’re like this too, you will enjoy this chapter. If not, you can just
skip it.
This chapter won’t introduce any new tools that you might use. It
is purely explanatory. It tries to explain to a satisfactory level how
coroutines work. The key lessons are:

• Suspending functions are like state machines, with a possible


state at the beginning of the function and after each suspend-
ing function call.
• Both the number identifying the state and the local data are
kept in the continuation object.
• 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.

If you are interested in learning some internals (simplified, of


course), read on.

Continuation-passing style

There are a few ways in which suspending functions could have


been implemented, but the Kotlin team decided on an option
called continuation-passing style. This means that continuations
(explained in the previous chapter) are passed from function to
function as arguments. By convention, a continuation takes the last
parameter position.
Coroutines under the hood 39

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

You might have also noticed that the result type under the hood is
different from the originally declared one. It has changed to 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.

A very simple function

To dive deeper, let’s start with a very simple function that prints
something before and after a delay.

suspend fun myFunction() {


println("Before")
delay(1000) // suspending
println("After")
}

You can already deduce how the myFunction function signature will
look under the hood:

fun myFunction(continuation: Continuation<*>): Any


Coroutines under the hood 40

The next thing is that this function needs its own continuation in
order to remember its state. Let’s name it 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)

This should be done only if the continuation isn’t wrapped already.


If it is, this is part of the resume process, and we should keep the
continuation unchanged¹³ (this might be confusing now, but later
you will better see why).

val continuation =
if (continuation is MyFunctionContinuation) continuation
else MyFunctionContinuation(continuation)

This condition can be simplified to:

val continuation = continuation as? MyFunctionContinuation


?: MyFunctionContinuation(continuation)

Finally, let’s talk about the body of our function.

suspend fun myFunction() {


println("Before")
delay(1000) // suspending
println("After")
}

The function could be started from two places: either from the be-
ginning (in the case of a first call) or from the point after suspension
(in the case of resuming from continuation). To identify the current
state, we use a field called 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.
¹³The actual mechanism here is a little more complicated as the
first bit of the label is also changed, and this change is checked by
the suspending function. This mechanism is needed for suspending
functions in order to support recurrence. This has been skipped for
the sake of simplicity.
Coroutines under the hood 41

// 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")
}

The last important piece is also presented in the snippet above.


When 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¹⁴. This is how a
suspension ends all these functions and leaves the thread available
for other runnables (including coroutines) to be used.
Before we go any further, let’s analyze the code above. What would
happen if this delay call didn’t return COROUTINE_SUSPENDED? What
if it just returned Unit instead (we know it won’t, but let’s hypothe-
size)? Notice that if the delay just returned Unit, we would just move
to the next state, and the function would behave like any other.
Now, let’s talk about the continuation, which is implemented as an
anonymous class. Simplified, it looks like this:

¹⁴More concretely, COROUTINE_SUSPENDED is propagated until it


reaches either the builder function or the ‘resume’ function.
Coroutines under the hood 42

cont = object : ContinuationImpl(continuation) {


var result: Any? = null
var label = 0

override fun invokeSuspend(`$result`: Any?): Any? {


this.result = `$result`;
return myFunction(this);
}
};

To improve the readability of our function, I decided to represent it


as a class named MyFunctionContinuation. I also decided to hide the
inheritance by inlining the ContinuationImpl body. The resulting
class is simplified: I’ve skipped many optimizations and functionali-
ties so as to keep only what is essential.

In JVM, type arguments are erased during compilation;


so, for instance, both Continuation<Unit> or
Continuation<String> become just Continuation. Since
everything we present here is Kotlin representation of
JVM bytecode, you should not worry about these type
arguments at all.

The code below presents a complete simplification of how our func-


tion 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
Coroutines under the hood 43

}
error("Impossible")
}

class MyFunctionContinuation(
val completion: Continuation<Unit>
) : Continuation<Unit> {
override val context: CoroutineContext
get() = completion.context

var label = 0

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 = myFunction(token, this)
if (r == COROUTINE_SUSPENDED) return
Result.success(r as Unit)
} catch (e: Throwable) {
Result.failure(e)
}
completion.resumeWith(res)
}
}

If you want to analyze by yourself what suspending functions are


under the hood, open the function in IntelliJ IDEA, use Tools >
Kotlin > Show Kotlin bytecode, and click the “Decompile” button. As
a result, you will see this code decompiled to Java (so more or less
how this code would look if it were written in Java).
Coroutines under the hood 44

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.
Coroutines under the hood 45

Bytecode from the Kotlin suspending function decompiled into Java.

A function with a state

If a function has some state (like local variables or parameters) that


needs to be restored after suspension, this state needs to be kept in
this function’s continuation. Let’s consider the following function:

suspend fun myFunction() {


println("Before")
var counter = 0
delay(1000) // suspending
counter++
println("Counter: $counter")
println("After")
}

Here counter 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 be-
fore suspension. Restoring these kinds of properties happens at the
beginning of the function. So, this is how the (simplified) function
looks under the hood:

You might also like