Aldo Wachyudi
Aldo Wachyudi

Reputation: 17991

How to properly handle cancellation in coroutine's computation code?

Here is my understanding of cancellation in coroutine:

If a parent coroutine is canceled, the children will stop too. If a child coroutine throws Exception, the sibling and parent coroutine will notice it and stop.

Except for SupervisorJob, it will continue active even though one of the child coroutines is stopped.

So, I write a code snippet to practice my understanding.

Code Snippet 1

fun main() {
    val parentScope = CoroutineScope(SupervisorJob())
    parentScope.launch {
        val childJob = launch {
            try {
                println("#1")
                Thread.sleep(1_000)
                println("#2")
            } catch (e: Exception) {
                println("#3")
            }
        }
        println("#4")
        childJob.cancel()
    }
    Thread.sleep(2_000)
}

Here are two of my expectations:

Expectation 1:

#1 is called first because there's no blocking code between child and parent job.
#4 is called because `Thread.sleep` is blocking.
#3 is called because the childJob is cancelled, even though the coroutine is not finished.

Expectation 2:

#4 is called first because the parent coroutine start first.
#1 is called because even though the childJob is cancelled, there's time for #1 to be executed.

However, the actual output of code snippet 1 is:

#4
#1
#2

I read coroutine docs again to find out that for computation code, we have to either use yield or check the coroutine state (active, canceled, isCompleted). Then I make the following adjustment:

Code Snippet 2

fun main() {
    val parentScope = CoroutineScope(SupervisorJob())
    parentScope.launch {
        val childJob = launch {
            try {
                println("#1")
                Thread.sleep(1_000)
                if (isActive) {
                    println("#2")
                }
            } catch (e: Exception) {
                println("#3")
            }
        }
        println("#4")
        childJob.cancel()
    }
    Thread.sleep(2_000)
}

This time the output is:

#4
#1

Here are my questions:

  1. In code snippet 1, how is #2 still executed after childJob is canceled?

  2. In code snippet 1, why #3 is never executed even though childJob is called?

  3. In code snippet 2, do we really need to use yield or checking coroutine state every time we want to execute a coroutine code? Because in my opinion, the code will be harder to read.

  4. Is there something wrong my code snippet or my understanding of coroutine?

Note: I don't want to use GlobalScope.runBlocking for the code snippet, because in the real project, we don't use GlobalScope anyway. I want to create an example as close as what a real project should be, using parent-child scoping with some lifecycle.

Upvotes: 1

Views: 832

Answers (1)

Valeriy Katkov
Valeriy Katkov

Reputation: 40582

In code snippet 1, how is #2 still executed after childJob is canceled?

Only suspending functions are cancellable. Replace Thread.sleep(1_000) with suspending delay and it would be cancelled.

In code snippet 1, why #3 is never executed even though childJob is called?

It's because the coroutine wasn't cancelled. See the first question answer.

In code snippet 2, do we really need to use yield or checking coroutine state every time we want to execute a coroutine code? Because in my opinion, the code will be harder to read.

No, you shouldn't. Every suspending function checks for cancellation of coroutine.

Is there something wrong my code snippet or my understanding of coroutine?

It's essential to understand suspending functions when you work with coroutines. Coroutine Basics documentation section explains it pretty well. And may be the SO question would be useful.

Upvotes: 1

Related Questions