Coroutines cancellation

Yahya Hassani
4 min readNov 17, 2022

--

In multithread programming, a good cancellation mechanism is worth a lot because you may face a situation that checks a state to kill a thread without an opportunity to clean up resources. In this article, we will explore the coroutine cancellation mechanism which is surprisingly simple and convenient. we will cover how we can cancel a coroutine, how the cancellation happens, and how to handle clean-up operations. This article is based on the Kotlin Coroutines book by Marcin Moskala.

To understand coroutine cancellation, first, let's have a look at the coroutine lifecycle.

As you see, the coroutine lifecycle is started in a NEW or ACTIVE state. All coroutines will start in an ACTIVE state unless we start them lazily. A coroutine can be cancelled from the ACTIVE or COMPLETING state, and when it happens it won't cancel and finished the coroutine immediately, but it will go to the CANCELLING state, which can do clean-up operations like the closing file or databases connection in this state. Now that we know the coroutine lifecycle, we can deep dive into the cancellation procedure.

Basic Cancellation

When we launch a coroutine it will return a reference to the coroutine as a Job. The coroutine is cancelled when the resulting job is cancelled, so we can cancel a coroutine by cancelling its job. But what happens to a coroutine when we cancel it? cancelling a coroutine triggers the following effects:

  • Such a coroutine ends the job at the first suspension point(delay in the following example)
  • If a job has some children, they are also cancelled but its parent is not affected
  • Once a job is cancelled, it cannot be used as a parent for any new coroutines

Look at the following example.

Cancel a coroutine

The output is:

printing 0
printing 1
printing 2
printing 3
printing 4
Canceled successfully

When we cancel a job it will go to cancelling state, which may this state take time(clean-up operation), and if we do not wait for this state to be finished, we would have some race conditions, and the output becomes like this:

printing 0
printing 1
printing 2
printing 3
Canceled successfully
printing 4

Sometimes it is important to be sure that the job is correctly cancelled. If we want to be sure about it, we should wait by calling Join() function after cancelling it.

job.cancel()
job.join()

Or we can call the following extension function which does both:

job.cancelAndJoin()

We often need to cancel a group of concurrent tasks, for example in android we cancel all the coroutines started in a view when a user leaves this view. To cancel a group of coroutines we should start all of them with the same context, and cancel them altogether. look at the following example.

Cancel a group of coroutines

The output is:

printing task_1 1
printing task_2 1
printing task_1 2
Canceled successfully

Cancel unstoppable

As we said cancellation happens on suspension points, so it will not happen if there is no suspension point. In the following example, we use the sleep() method to simulate a complex operation(do not use it in a real project). This coroutine will never stop as there is no suspension point.

Unstoppable coroutine

The output is:

printing 0
printing 1
printing 2
printing 3
// ... up to 1000

To cancel this kind of coroutines we have two options. We can create a suspension point using the yield() function which suspends and immediately resume a coroutine. This gives an opportunity to do the cancellation. You just need to call the yield() method inside the coroutine. Another option is to track the state of the job and check if the job is still active and stop the coroutine when it is inactive. We can do this by calling the ensureActive() method inside the coroutine which will check the job state and throw a CancellationException if the job is inactive. Look at the following code snippet.

Cancel an unstoppable coroutine

The output is:

printing 1
printing 2
printing 3
Canceled successfully

Clean up

When a job is cancelled, it changes its state to CANCELLING and at the first suspension point, a CancellationException is thrown. So, cancelling a job is not just stopped, it is cancelled internally with an exception. Therefore, we can clean up everything inside finally block.

The output is:

Done
Will always be printed
Canceled successfully

(or)
Will always be printed
Canceled successfully

But doing clean-up inside the finally block has limitations. We can clean up all the resources, but starting a new coroutine or suspension is not allowed. If we start a coroutine, it will be ignored and If we try to suspend it, it will throw a CancellationException.

Long operation in finally block

The output is:

Finally
Canceled successfully

Sometimes we truly need to do long operations inside the finally block, like rollback changes in the database. To do that, the first option is to do the operation inside a NonCancellable coroutine. We can do this by wrapping the operation with the WithConctext(NonCancellable) function.

The output is:

Finally
will printed
Clean up done
Canceled successfully

Another mechanism is the invokOnCompletion function from a job, which set a handler to be called when the job reaches a terminal state, either Completed or cancelled. One of this handler's parameters is an exception which if it is null, means the coroutine is finished with no exception, if it is CancellationException, means the coroutine was cancelled, and other kinds of exception mean the exception that finished the coroutine.

Clean up with invokeOnCompletion

The output is:

Finished
will always be printed
The exception was: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelled}@3e02c794
Canceled successfully

Wrap up

In this article, we try to talk about the coroutine cancellation mechanisms and clean-up after cancellation. I hope it helps.

--

--