Blog Infos
Author
Published
Topics
, , , ,
Published

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Practical Patterns for Kotlin Coroutines in Production

Unlock the full potential of Kotlin Coroutines with a focused exploration of their practical applications in real-world scenarios. This talk will guide you through essential best practices, demonstrate robust patterns for common asynchronous tasks, and…
Watch Video

Practical Patterns for Kotlin Coroutines in Production

Marcin Moskała
Developer during the day, author at night, trainer
Kt. Academy

Practical Patterns for Kotlin Coroutines in Production

Marcin Moskała
Developer during the ...
Kt. Academy

Practical Patterns for Kotlin Coroutines in Production

Marcin Moskała
Developer during the day, ...
Kt. Academy

Jobs

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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu