Kotlin Coroutines [Part-2]

Bharath Kandula
Tilicho Labs
4 min readFeb 18, 2022

--

This blog is a part of Kotlin Coroutines series. If you want to check Part-1:

Continuing…

Cancellation and timeouts

Cancellation

When launching multiple coroutines, it can be a pain to keep track of them or cancel each individually. Rather, we can rely on canceling the entire scope coroutines are launched into as this will cancel all of the child coroutines created:

// assume we have a scope defined for this layer of the app
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()

Canceling the scope cancels its children

Sometimes you might need to cancel only one coroutine.
Calling job1.cancel ensures that only that specific coroutine gets canceled and all the other siblings are not affected:

// assume we have a scope defined for this layer of the app
val job1 = scope.launch { … }
val job2 = scope.launch { … }
// First coroutine will be cancelled and the other one won’t be affected
job1.cancel()

A canceled child doesn’t affect other siblings

Coroutines handle cancellation by throwing a special exception: CancellationException. If you want to provide more details on the cancellation reason you can provide an instance of CancellationException when calling .cancel as this is the full method signature:

fun cancel(cause: CancellationException? = null)

If you don’t provide your own CancellationException instance, a default CancellationException will be created

public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}

Once you cancel a scope, you won’t be able to launch new coroutines in the canceled scope.

If we just call cancel, it doesn’t mean that the coroutine work will just stop. If you’re performing some relatively heavy computation, like reading from multiple files, there’s nothing that automatically stops your code from running.

Let’s take a more simple example and see what happens. Let’s say that we need to print “Hello” twice a second using coroutines. We’re going to let the coroutine run for a second and then cancel it. One version of the implementation can look like this:

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val job = launch (Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("Hello ${i++}")
nextPrintTime += 500L
}
}
}
delay(1000L)
println("Cancel!")
job.cancel()
println("Done!")
}

Output:
Hello 0
Hello 1
Hello 2
Cancel!
Done!
Hello 3
Hello 4

Let’s see what happens step by step:

When calling launch, we’re creating a new coroutine in the active state. We’re letting the coroutine run for 1000ms. So now we see printed:

Output:
Hello 0
Hello 1
Hello 2

Once job.cancel is called, our coroutine moves to Cancelling state. But then, we see that Hello 3 and Hello 4 are printed to the terminal. Only after the work is done, the coroutine moves to a Cancelled state.

The coroutine work doesn’t just stop when cancel is called.

Checking for job’s active state ( job.isActive or ensureActive() )

One option is in our while(i<5) to add another check for the coroutine state:

// Since we're in the launch block, we have access to job.isActive
while (i < 5 && isActive)

This means that our work should only be executed while the coroutine is active.

The Coroutines library provides another helpful method — ensureActive(). Its implementation is:

fun Job.ensureActive(): Unit {
if (!isActive) {
throw getCancellationException()
}
}

Because this method instantaneously throws if the job is not active, we can make this the first thing we do in our while loop:

while (i < 5) {
ensureActive()

}

By using ensureActive, you avoid implementing the if statement required by isActive yourself, decreasing the amount of boilerplate code you need to write.

Timeout

The TimeoutCancellationException that is thrown by withTimeout is a subclass of CancellationException.

withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}

Output:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

Cancellation is just an exception, all resources are closed in the usual way. You can wrap the code with timeout in a try {...} catch (e: TimeoutCancellationException) {...} block if you need to do some additional action specifically on any kind of timeout or use the withTimeoutOrNull function that is similar to withTimeout but returns null on timeout instead of throwing an exception:

val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // will get cancelled before it produces this result
}
println("Result is $result")

There is no longer an exception when running this code

Output:
I'm sleeping 0 ...
I'm sleeping 1...
I'm sleeping 2...
Result is null

References

--

--