Cancellation is a crucial feature of Kotlin coroutines for managing resources and stopping them when they are no longer needed. A practical example when cancellation would be needed can be when the page that launched the coroutines has closed. The result of the coroutine is no longer needed and it’s operations can be cancelled. So, how do we cancel a coroutine??
As we know, Kotlin’s launch
function returns a Job
. Just like we use Job
to start a coroutine, we can use it to stop a coroutine as well.
fun cancelCoroutine() {
runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: Working $i ...")
delay(500L)
}
}
delay(2100L) // delay a bit
println("I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion
println("Now I can quit.")
}
}
As soon as main invokes job.cancel(), we don’t see any output from the coroutine after that because it was cancelled.
Note: We can also use
cancelAndJoin extension function that combines cancel and join invocations.
Coroutine Cancellation Challenges During Computation
All the suspend functions inside a coroutine are cancellable. They check for cancellation of coroutine and throw CancellationException when cancelled. However, if a coroutine is working on a computation, it will not check for cancellation, hence can not be cancelled. Let’s see an example for the same:
fun nonCancellableCoroutine() = runBlocking {
var sum = 0
val job = launch(Dispatchers.Default) {
for (i in 1..1000) {
sum += i
println("Partial sum after $i iterations: $sum")
}
}
delay(500)
println("I'm tired of waiting!")
job.cancelAndJoin()
println("Now I can quit.")
}
When you run this code, you will see that even after you call cancelAndJoin()
, it continues to print until it completes it’s 1000 iterations.
Now, let’s add a delay after each iteration of the loop and see what happens
fun cancellableCoroutineWithSuspendFunction() = runBlocking {
var sum = 0
val job = launch(Dispatchers.Default) {
for (i in 1..1000) {
sum += i
println("Partial sum after $i iterations: $sum")
delay(500)
}
}
println("I'm tired of waiting!")
job.cancelAndJoin()
println("Now I can quit.")
}
When I add a delay
in the loop, the loop get’s cancelled before completing because delay
is a suspend function and as I mentioned above all suspend functions are cancellable.
Now, let’s try and catch the Exception here:
fun catchExceptionInACoroutineWithSuspendFunction() = runBlocking {
var sum = 0
val job = launch(Dispatchers.Default) {
for (i in 1..1000) {
try {
sum += i
println("Partial sum after $i iterations: $sum")
delay(500)
} catch (e: Exception) {
println(e)
}
}
}
println("I'm tired of waiting!")
job.cancelAndJoin()
println("Now I can quit.")
}
The delay
function checks for cancellation and throws a CancellationException which we have catched and handled in our code. That is why the loop completes this time.
While it is generally discouraged to catch the broad ‘Exception
‘ class without specifying a more precise exception type because it may lead to unexpected consequences. But it is safer to use ‘Exception’ when using runCatching
as it is designed to catch exceptions during a block of code and wrap them in a ‘Result
‘ object.
Note: We will go into more details on
runCatching when we will discuss about exception handling in detail.
Making Computation code cancellable
There are two ways to make computation code cancellable. Let’s see both of them individually.
Using Yield function
Yield
function is a suspending function that is used to voluntarily pause the execution of the current coroutine, allowing other coroutines to run.
fun cancelCoroutineWithYield() = runBlocking {
var sum = 0
val job = launch(Dispatchers.Default) {
for (i in 1..1000) {
yield()
sum += i
println("Partial sum after $i iterations: $sum")
}
}
println("I'm tired of waiting!")
job.cancelAndJoin()
println("Now I can quit.")
}
The yield
function is not typically used for cancelling computations in a coroutine but since yield is a suspend function that pauses coroutine, it responds to the cancellation request and allows the coroutine to be cancelled gracefully.
Job Offers
Explicitly checking Cancellation status
The best way to cancel coroutines is using isActive
property. Coroutines can periodically check isActive
during their execution and gracefully exit if they detect that cancellation has been requested.
fun cancelCoroutineWithIsActive() = runBlocking {
var sum = 0
val job = launch(Dispatchers.Default) {
for (i in 1..1000) {
if(isActive) {
sum += i
println("Partial sum after $i iterations: $sum")
}
}
}
println("I'm tired of waiting!")
job.cancelAndJoin()
println("Now I can quit.")
}
https://miro.medium.com/v2/resize:fit:750/format:webp/0*2rVXQJNbfZCI6FFN
Closing resources with finally
Whenever a coroutine is cancelled, in order to make sure that the suspend functions perform their finalisation actions normally, we can use either of the two ways:
Finally block
fun closeResourcesInFinally() = runBlocking {
var sum = 0
val job = launch(Dispatchers.Default) {
try {
for (i in 1..1000) {
if (isActive) {
sum += i
println("Partial sum after $i iterations: $sum")
}
delay(500)
}
} catch (e: CancellationException) {
println("Coroutine canceled: $e")
} finally {
println("Cancellable function: Finally block executed")
}
}
delay(1000)
println("I'm tired of waiting!")
job.cancelAndJoin()
println("Now I can quit.")
}
Both join()
and cancelAndJoin()
wait for all the finalisation actions to complete, so the above example produces the following output:
When the coroutine is canceled using job.cancelAndJoin()
after a delay, the catch
block is executed, catching the CancellationException
. The finally
block is also executed, demonstrating that finalisation actions can be performed even during cancellation.
Use function
use
function is an extension function on Closeable
resources in Kotlin. It is used to manage resources such as files, network connections or any other resource that implements the Closeable
interface. It will ensure that the resource is properly closed, just like try-catch-finally block, after the function has been completed, whether any exception was raised or not.
fun closeResourcesUsingUse() = runBlocking {
val job = launch {
try {
val lines = readFileAsync("example.txt")
lines.forEach { println(it) }
} catch (e: Exception) {
println("Error reading file: $e")
}
}
delay(1000)
job.cancelAndJoin()
println("Job cancelled and joined.")
}
suspend fun readFileAsync(filename: String): List<String> = coroutineScope {
return@coroutineScope withContext(Dispatchers.Default) {
withContext(Dispatchers.IO) {
BufferedReader(FileReader(filename)).use { reader ->
val lines = mutableListOf<String>()
var line: String? = reader.readLine()
while (line != null) {
lines.add(line)
line = reader.readLine()
}
lines
}
}
}
}
The BufferedReader
is wrapped in the use
function, ensuring that it is properly closed after the lines are read.
Run non-cancellable block
If we try to call a suspend function in the finally block of the above example, we will get a CancellationException because the coroutine running this code is cancelled.
Usually, we will not find any need to call a suspend function in the finally block because all well-behaving closing operations like closing a file, closing any communication channel or cancelling a job are non-blocking and do not involve any suspending functions.
However if we ever find a need to call suspend function in the finally block, we can do so by wrapping our code inside withContext(NonCancellable)
block like this:
fun runNonCancellableBlock() = runBlocking{
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
The NonCancellable
context is a context in which the coroutine is not cancellable. This means even if the cancellation is requested, the coroutine will continue executing the code within this context. After the NonCancellable block finishes, the cancellation state will restore.
Timeout
If we want to cancel a coroutine after a specified amount of time, like many examples above, we can do so manually by keeping a reference to our coroutine and then calling cancel()
or cancelAndJoin()
functions to it.
But, Kotlin provides us with a ready to use function for our such requirements, withTimeout
fun cancelAfterTimeout() = runBlocking {
withTimeout(1500){
repeat(1000){i ->
println("job: I'm sleeping $i ...")
delay(400L)
}
}
}
After 1500 milliseconds, this coroutine will cancel and throw TimeoutCancellationException which is a subclass of CancellationException.
https://miro.medium.com/v2/resize:fit:750/format:webp/0*sW5dcRL9J2N8VvSZ
That’s it for this article. Hope it was helpful! If you like it, please hit like.
This article is previously published on proandroidev.com