0% found this document useful (0 votes)
12 views

Coroutines 4

Uploaded by

Alex Trujillo
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views

Coroutines 4

Uploaded by

Alex Trujillo
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

How does suspension work?

26

How does suspension work?

Suspending functions are the hallmark of Kotlin coroutines. The


suspension capability is the single most essential feature upon which
all other Kotlin Coroutines concepts are built. That is why our goal
in this chapter is to forge a solid understanding of how it works.
Suspending a coroutine means stopping it in the middle. It is similar
to stopping a video game: you save at a checkpoint, turn off the game,
and both you and your computer can focus on doing different things.
Then, when you would like to continue some time later, you turn
on the game again, resume from the saved checkpoint, and thus you
can play from where you previously left off. This is an analogy to
coroutines. When they are suspended, they return a Continuation.
It is like a save in a game: we can use it to continue from the point
where we stopped.
Notice that this is very different from a thread, which cannot be
saved, only blocked. A coroutine is much more powerful. When
suspended, it does not consume any resources. A coroutine can be
resumed on a different thread, and (at least in theory) a continuation
can be serialized, deserialized and then resumed.

Resume

So let’s see it in action. For this, we need a coroutine. We start


coroutines using coroutine builders (like runBlocking or launch),
which we will introduce later. Although there is also a simpler way,
we can use a suspending main function.
Suspending functions are functions that can suspend a coroutine.
This means that they must be called from a coroutine (or another
suspending function). In the end, they need to have something to
suspend. Function main is the starting point, so Kotlin will start it in
a coroutine when we run it.

suspend fun main() {


println("Before")

println("After")
}
// Before
// After
How does suspension work? 27

This is a simple program that will print “Before” and “After”. What
will happen if we suspend in between these two prints? For that,
we can use the suspendCoroutine function provided by the standard
Kotlin library⁹.

suspend fun main() {


println("Before")

suspendCoroutine<Unit> { }

println("After")
}
// Before

If you call the above code, you will not see the “After”, and the code
will not stop running (as our main function never finished). The
coroutine is suspended after “Before”. Our game was stopped and
never resumed. So, how can we resume? Where is this aforemen-
tioned Continuation?
Take a look again at the suspendCoroutine invocation and notice that
it ends with a lambda expression ({ }). The function passed as an
argument will be invoked before the suspension. This function gets
a continuation as an argument.

suspend fun main() {


println("Before")

suspendCoroutine<Unit> { continuation ->


println("Before too")
}

println("After")
}
// Before
// Before too

⁹It directly calls suspendCoroutineUninterceptedOrReturn,


which is a primitive function, that means a function with intrinsic
implementation.
How does suspension work? 28

Such a function calling another function in place is nothing new.


This is similar to let, apply, or useLines. The suspendCoroutine
function is designed in the same way, which makes it possible to use
continuation just before the suspension. After the suspendCoroutine
call, it would be too late. So, the lambda expression passed as a
parameter to the suspendCoroutine function is invoked just before
the suspension. This lambda is used to store this continuation some-
where or to plan whether to resume it.
We could use it to resume immediately:

suspend fun main() {


println("Before")

suspendCoroutine<Unit> { continuation ->


continuation.resume(Unit)
}

println("After")
}
// Before
// After

Notice that “After” in the example above is printed because we call


resume in suspendCoroutine¹⁰.

Since Kotlin 1.3, the definition of Continuation has been


changed. Instead of resume and resumeWithException,
there is one resumeWith function that expects Result.
The resume and resumeWithException functions we are
using are extension functions from the standard library
that use resumeWith.

¹⁰This statement is true, but I need to clarify. You might imagine


that here we suspend and immediately resume. This is a good intu-
ition, but the truth is that there is an optimization that prevents a
suspension if resuming is immediate.
How does suspension work? 29

inline fun <T> Continuation<T>.resume(value: T): Unit =


resumeWith(Result.success(value))

inline fun <T> Continuation<T>.resumeWithException(


exception: Throwable
): Unit = resumeWith(Result.failure(exception))

We could also start a different thread that will sleep for a set duration
and resume after that time:

suspend fun main() {


println("Before")

suspendCoroutine<Unit> { continuation ->


thread {
println("Suspended")
Thread.sleep(1000)
continuation.resume(Unit)
println("Resumed")
}
}

println("After")
}
// Before
// Suspended
// (1 second delay)
// After
// Resumed

This is an important observation. Notice that we can make a func-


tion that will resume our continuation after a defined period. In
such a case, the continuation is captured by the lambda expression,
as shown in the code snippet below.
How does suspension work? 30

fun continueAfterSecond(continuation: Continuation<Unit>) {


thread {
Thread.sleep(1000)
continuation.resume(Unit)
}
}

suspend fun main() {


println("Before")

suspendCoroutine<Unit> { continuation ->


continueAfterSecond(continuation)
}

println("After")
}
// Before
// (1 sec)
// After
//sampleEnd

Such a mechanism works, but it unnecessarily creates threads only


to end them after just a second of inactivity. Threads are not cheap,
so why waste them? A better way would be to set up an “alarm clock”.
In JVM, we can use ScheduledExecutorService for that. We can set
it to call some continuation.resume(Unit) after a defined amount of
time.

private val executor =


Executors.newSingleThreadScheduledExecutor {
Thread(it, "scheduler").apply { isDaemon = true }
}

suspend fun main() {


println("Before")

suspendCoroutine<Unit> { continuation ->


executor.schedule({
continuation.resume(Unit)
How does suspension work? 31

}, 1000, TimeUnit.MILLISECONDS)
}

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

Suspending for a set amount of time seems like a useful feature. Let’s
extract it into a function. We will name it delay.

private val executor =


Executors.newSingleThreadScheduledExecutor {
Thread(it, "scheduler").apply { isDaemon = true }
}

suspend fun delay(timeMillis: Long): Unit =


suspendCoroutine { cont ->
executor.schedule({
cont.resume(Unit)
}, timeMillis, TimeUnit.MILLISECONDS)
}

suspend fun main() {


println("Before")

delay(1000)

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

The executor still uses a thread, but it is one thread for all coroutines
using the delay function. This is much better than blocking one
thread every time we need to wait for some time.
How does suspension work? 32

This is exactly how delay from the Kotlin Coroutines library used to
be implemented. The current implementation is more complicated,
mainly so as to support testing, but the essential idea remains the
same.

Resuming with a value

One thing that might concern you is why we passed Unit to the
resume function. You might also be wondering why we used Unit as a
type argument for the suspendCoroutine. The fact that these two are
the same is no coincidence. Unit is also returned from the function
and is the generic type of the Continuation parameter.

val ret: Unit =


suspendCoroutine<Unit> { cont: Continuation<Unit> ->
cont.resume(Unit)
}

When we call suspendCoroutine, we can specify which type will be


returned in its continuation. The same type needs to be used when
we call resume.

suspend fun main() {


val i: Int = suspendCoroutine<Int> { cont ->
cont.resume(42)
}
println(i) // 42

val str: String = suspendCoroutine<String> { cont ->


cont.resume("Some text")
}
println(str) // Some text

val b: Boolean = suspendCoroutine<Boolean> { cont ->


cont.resume(true)
}
println(b) // true
}
How does suspension work? 33

This does not fit well with the game analogy. I don’t know of any
game in which you can put something inside the game when re-
suming a save¹¹ (unless you cheated and googled how to solve the
next challenge). However, it makes perfect sense with coroutines.
Often we are suspended because we are waiting for some data, such
as a network response from an API. This is a common scenario.
Your thread is running business logic until it reaches a point where
it needs some data. So, it asks your network library to deliver it.
Without coroutines, this thread would then need to sit and wait.
This would be a huge waste as threads are expensive, especially if
this is an important thread, like the Main Thread on Android. With
coroutines, it just suspends and gives the library a continuation with
the instruction “Once you’ve got this data, just send it to the resume
function”. Then the thread can go do other things. Once the data is
there, the thread will be used to resume from the point where the
coroutine was suspended.
To see this in action, let’s see how we might suspend until we re-
ceive some data. In the example below, we use a callback function
requestUser that is implemented externally.

suspend fun main() {


println("Before")
val user = suspendCoroutine<User> { cont ->
requestUser { user ->
cont.resume(user)
}
}
println(user)
println("After")
}
// Before
// (1 second delay)
// User(name=Test)
// After

¹¹During a workshop discussion it turned out there is such a game:


in Don’t Starve Together, when you resume, you can change players.
I haven’t played it myself, but this sounds like a nice metaphor for
resuming with a value.
How does suspension work? 34

Calling suspendCoroutine directly is not convenient. We would pre-


fer to have a suspending function instead. We can extract it our-
selves.

suspend fun requestUser(): User {


return suspendCoroutine<User> { cont ->
requestUser { user ->
cont.resume(user)
}
}
}

suspend fun main() {


println("Before")
val user = requestUser()
println(user)
println("After")
}

Currently, suspending functions are already supported by many pop-


ular libraries, such as Retrofit and Room. This is why we rarely need
to use callback functions in suspending functions. However, if you
have such a need, I recommend using suspendCancellableCoroutine
(instead of suspendCoroutine), which will be explained in the Cancel-
lation chapter.

suspend fun requestUser(): User {


return suspendCancellableCoroutine<User> { cont ->
requestUser { user ->
cont.resume(user)
}
}
}

You might wonder what happens if the API gives us not data but
some kind of problem. What if the service is dead or responds
with an error? In such a case, we cannot return data; instead, we
should throw an exception from the place where the coroutine was
suspended. This is where we need to resume with an exception.
How does suspension work? 35

Resume with an exception

Every function we call might return some value or throw an excep-


tion. The same is true for suspendCoroutine. When resume is called,
it returns data passed as an argument. When resumeWithException
is called, the exception that is passed as an argument is conceptually
thrown from the suspension point.

class MyException : Throwable("Just an exception")

suspend fun main() {


try {
suspendCoroutine<Unit> { cont ->
cont.resumeWithException(MyException())
}
} catch (e: MyException) {
println("Caught!")
}
}
// Caught!

This mechanism is used for different kinds of problems. For instance,


to signal network exceptions.

suspend fun requestUser(): User {


return suspendCancellableCoroutine<User> { cont ->
requestUser { resp ->
if (resp.isSuccessful) {
cont.resume(resp.data)
} else {
val e = ApiException(
resp.code,
resp.message
)
cont.resumeWithException(e)
}
}
}
}

You might also like