But what does suspend mean?
Functions marked with the suspend
keyword are transformed at compile time to be made asynchronous under the hood (in bytecode), even though they appear synchronous in the source code.
The best source to understand this transformation IMO is the talk "Deep Dive into Coroutines" by Roman Elizarov.
For example, this function:
class MyClass {
suspend fun myFunction(arg: Int): String {
delay(100)
return "bob"
}
}
is turned into (expressed in Java instead of actual JVM bytecode for simplicity):
public final class MyClass {
public final Object myFunction(int arg, @NotNull Continuation<? super String> $completion) {
// body turned into a state machine, hidden for brevity
}
}
This includes the following changes to the function:
- The return type is changed to Java's
Object
(the equivalent of Kotlin's Any?
- a type containing all values), to allow returning a special COROUTINE_SUSPENDED
token to represent when the coroutine is actually suspended
- It gets an additional
Continuation<X>
argument (where X is the former return type of the function that was declared in the code - in the example it's String
). This continuation acts like a callback when resuming the suspend function.
- Its body is turned into a state machine (instead of literally using callbacks, for efficiency). This is done by breaking down the body of the function into parts around so called suspension points, and turning those parts into the branches of a big switch. The state about the local variables and where we are in the switch is stored inside the
Continuation
object.
This is a very quick way to describe it, but you can see it happen with more details and with examples in the talk. This whole transformation is basically how the "suspend/resume" mechanism is implemented under the hood.
Coroutine or function gets suspended?
At a high level, we say that calling a suspending function suspends the coroutine, meaning the current thread can start executing another coroutine. So, the coroutine is said to be suspended rather than the function.
In fact, call sites of suspending functions are called "suspension points" for this reason.
Which coroutine gets suspended?
Let's look at your code and break down what happens (the numbering follows the execution timeline):
// 1. this call starts a new coroutine (let's call it C1).
// If there were code after it, it would be executed concurrently with
// the body of this async
async {
...
// 2. this is a regular function call, so we go to computation()'s body
val deferred = computation()
// 4. we're back from the call to computation, about to call await()
// Because await() is suspendING, it suspends coroutine C1.
// This means that if we had a single thread in our dispatcher,
// it would now be free to go execute C2. With multiple threads,
// C2 may have already started executing. In any case we wait
// here for C2 to complete.
// 7. once C2 completes, C1 is resumed with the result `true` of C2's async
val result = deferred.await()
...
// 8. C1 can now keep going in the current thread until it gets
// suspended again (or not)
}
fun computation(): Deferred<Boolean> {
// 3. this async call starts a second coroutine (C2). Depending on the
// dispatcher you're using, you may have one or more threads.
// 3.a. If you have multiple threads, the block of this async could be
// executed in parallel of C1 in another thread
// 3.b. If you have only one thread, the block is sort of "queued" but
// not executed right away (as in an event loop)
//
// In both cases, we say that this block executes "concurrently"
// with C1, and computation() immediately returns the Deferred
// instance to its caller (unless a special dispatcher or
// coroutine start argument is used, but let's keep it simple).
return async {
// 5. this may now be executed
true
// 6. C2 is now completed, so the thread can go back to executing
// another coroutine (e.g. C1 here)
}
}
The outer async
starts a coroutine. When it calls computation()
, the inner async
starts a second coroutine. Then, the call to await()
suspends the execution of the outer async
coroutine, until the execution of the inner async
's coroutine is over.
You can even see that with a single thread: the thread will execute the outer async
's beginning, then call computation()
and reach the inner async
. At this point, the body of the inner async is skipped, and the thread continues executing the outer async
until it reaches await()
.
await()
is a "suspension point", because await
is a suspending function.
This means that the outer coroutine is suspended, and thus the thread starts executing the inner one. When it is done, it comes back to execute the end of the outer async
.
Does suspend mean that while outer async coroutine is waiting (await) for the inner computation coroutine to finish, it (the outer async coroutine) idles (hence the name suspend) and returns thread to the thread pool, and when the child computation coroutine finishes, it (the outer async coroutine) wakes up, takes another thread from the pool and continues?
Yes, precisely.
The way this is actually achieved is by turning every suspending function into a state machine, where each "state" corresponds to a suspension point inside this suspend function. Under the hood, the function can be called multiple times, with the information about which suspension point it should start executing from (you should really watch the video I linked for more info about that).