https://kotlinlang.org logo
#coroutines
Title
# coroutines
a

Aldo Wachyudi

08/03/2019, 2:49 AM
So far, I understand the basic about coroutine topic (CoroutineBuilder, CoroutineScope, Dispatchers. etc.). However, I find one topic in coroutine that is difficult for me to understand. It’s about the cancellation of coroutine(s). I wrote the detailed question on StackOverflow here: https://stackoverflow.com/questions/57335077/how-to-properly-handle-cancellation-in-coroutines-computation-code If you guys have time, please help me to answer the question. I will re-post the question on reply thread.
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
Copy code
kotlin
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:
Copy code
#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:
Copy code
#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:
Copy code
#4
#1
#2
I read [coroutine docs][1] 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
Copy code
kotlin
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:
Copy code
#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. [1]: https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html
k

Kevin Gorham

08/03/2019, 3:11 AM
use
delay
instead of
Thread.sleep
and you'll get the behavior you expect. If I get some time, I can explain why tomorrow (it's late in the night for me).
(short version: you're mixing blocking with suspending. Don't block, suspend. Delay suspends.)
a

Aldo Wachyudi

08/03/2019, 3:18 AM
Thanks for replying at late night Kevin 🙂 I use
Thread.sleep
to emulate blocking code in my project like fetching a JSON over network or compressing bitmap. For this, we should use
delay()
instead of
Thread.sleep
? Why the resulting behavior is different?
k

Kevin Gorham

08/03/2019, 3:19 AM
Also (in case it helps) you're catching the cancellation exception and that will cause some weirdness--but that's a little easier to understand. So avoid thread.sleep and don't catch the cancellation.
the blocking code in your project should suspend
if you can
a

Aldo Wachyudi

08/03/2019, 3:22 AM
So avoid thread.sleep and don’t catch the cancellation.
Then, I don’t need to explicitly wrap my code with
try
and
catch
for the sake of cancellation?
k

Kevin Gorham

08/03/2019, 3:23 AM
if you can't make your blocking code suspend, then yes, you'll probably have to deal with
isActive
(meaning you have to participate in cooperative cancellation "manually") but yeah, I'm sure others will step in to help🤞. I have to hop off. Here, this might help...

https://www.youtube.com/watch?v=YrrUCSi72E8

a little old but it explains cancellation
around the 30min mark
if you're integrating with existing code, you may have to step into the weeds a bit to bridge the gap between blocking code and suspending code. Extension functions are great for this--write once and then use them wherever you need to bridge the gap. Libraries like retrofit already do this for you. You can look at their extension functions for examples of working with callbacks: https://github.com/square/retrofit/blob/5c2a2c8a689b084be41056ed54c2612b2ae46c3f/retrofit/src/main/java/retrofit2/KotlinExtensions.kt
ok. now I really have to go. good luck!
a

Aldo Wachyudi

08/06/2019, 5:21 PM
I have replaced
Thread.sleep
with a suspending function that fetch a data over network, like this:
Copy code
fun main() = runBlocking {
    val parentScope = CoroutineScope(SupervisorJob())
    parentScope.launch {
        println("#1")
        launch {
            println("#3")
            fetchDataOverNetwork()
            println("#4")
        }
        println("#2")
    }
    delay(1_000)
    parentScope.cancel()
    delay(1_000)
    println("Bye")
}

suspend fun fetchDataOverNetwork() {
    val tenSecondInMillis = 10000
    val result = URL("<https://httpstat.us/200?sleep=$tenSecondInMillis>").readText()
    println("$result")
}
Now, the output is:
Copy code
#1
#2
#3
Bye
#4 is never printed because the coroutine is already cancelled.
However, I’m still confused about the
cancellation exception
part. If I put try-catch on the code.
Copy code
fun main() = runBlocking {
    val parentScope = CoroutineScope(SupervisorJob())
    parentScope.launch {
        try {
            println("#1")
            launch {
                try {
                    println("#3")
                    fetchDataOverNetwork()
                    println("#4")
                } catch (e: Exception) {
                    println("Child's Exception not printed")
                } finally {
                    println("Child's Finally not printed")
                }
            }
            println("#2")
        } catch (e: Exception) {
            println("Parent's Exception not printed")
        } finally {
            println("Parent's Finally is printed")
        }
    }
    delay(1_000)
    parentScope.cancel()
    delay(1_000)
    println("Bye")
}
The output is:
Copy code
#1
#2
Parent's Finally is printed
#3
Bye
Why is the exception and finally block is not executed? Even though in the documentation it says..
They check for cancellation of coroutine and throw CancellationException when cancelled.
Under what condition the
CancellationException
is thrown?
s

streetsofboston

08/06/2019, 6:02 PM
“Exceptional Exceptions for Coroutines made easy…?” by Anton Spaans https://link.medium.com/rQ01l9CdWY
a

Aldo Wachyudi

08/07/2019, 2:59 AM
Here @miqbaldc
3 Views