Photo by Bruno Kelzer on Unsplash
This is the last article in a series of blog posts about applying Structural concurrency.
- (This post) — Applying Kotlin Structured concurrency: Part IV — Coroutines Cancellation
Structured concurrency helps to cancel coroutines not only individually one by one but also centralised using root coroutine — cancelation of parent coroutine canceling child coroutines too.
CancellationException
Coroutine cancellation is throwing a CancellationException. A cancellation is just a specific type of exception that is treated differently from a failure.
There is a difference in CancellationException propagation in comparison with other exceptions — this exception cancels itself and child coroutines while other exceptions cancel itself, child, siblings and parent coroutines.
Job Offers
Avoid scope job cancellation
You can cancel parent scope with all child coroutines.
But after cancellation you can’t start coroutine again in that cancelled scope. If you cancelling scope — you cancel all child coroutines.
import kotlinx.coroutines.* val job = Job() val scope = CoroutineScope(Dispatchers.Default + job) fun main() { runBlocking { scope.launch { println("do work 1") delay(50) println("do more work 1") } delay(10) job.cancel() scope.launch { delay(10) println("do work 2") } } }
When we cancel job we put it into the completed state. Coroutines launched in a scope of the completed job will not be executed.
Cancel all coroutines in scope
When you want to cancel all coroutines of a specific scope, you can use cancelChildren()
function. Also, it’s a good practice to provide the possibility to cancel individual jobs.
import kotlinx.coroutines.* val job = Job() val scope = CoroutineScope(Dispatchers.Default + job) fun main() { runBlocking { scope.launch { println("do work 1") delay(50) println("do more work 1") } delay(10) scope.coroutineContext.cancelChildren() scope.launch { delay(10) println("do work 2") } } }
Cancel job
You can cancel specific coroutine without affecting siblings cancelling specific job.
import kotlinx.coroutines.* val job = Job() val scope = CoroutineScope(Dispatchers.Default + job) fun main() { runBlocking { val job1 = scope.launch { println("do work 1") delay(50) println("do more work 1") } val job2 = scope.launch { println("do work 2") delay(50) println("do more work 2") } delay(10) job1.cancel() } }
Cooperative cancellation
If you try to cancel coroutine during long operation you can not always get expected behaviour:
import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext private val context: CoroutineContext = Job() + Dispatchers.Default fun main() { CoroutineScope(context).launch { val job = launch { var i = 0 while (i < 4) { println("$i") Thread.sleep(60) i++ } } delay(100) println("Cancel") job.cancel() println("Done") } Thread.sleep(500) }
Coroutines cancellation is cooperative — so you need to check if coroutine was cancelled using job.isActive
or ensureActive()
. The difference between isActive
and ensureActive
is that the latter immediately throws a CancellationException
if the job is no longer active.
import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext private val context: CoroutineContext = Job() + Dispatchers.Default fun main() { CoroutineScope(context).launch { val job = launch { var i = 0 while (i < 4 && isActive) { println("$i") Thread.sleep(60) i++ } } delay(100) println("Cancel") job.cancel() println("Done") } Thread.sleep(500) }
But you can fix it even simpler: you can change Thread.sleep()
to delay()
then it starts to work as expected. Why? All suspend functions from kotlinx.coroutines
are cancellable: withContext
, delay
etc.
import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext private val context: CoroutineContext = Job() + Dispatchers.Default fun main() { CoroutineScope(context).launch { val job = launch { var i = 0 while (i < 4) { println("$i") delay(60) i++ } } delay(100) println("Cancel") job.cancel() println("Done") } Thread.sleep(500) }
There is one more useful function — yeild()
. In addition to checking the cancellation status of the job, the underlying thread is released and is made available for other coroutines.
Don’t catch cancellation exception
You should remember — coroutines cancellation works by trowing CancellationException
so you should have separate catch
for it.
You should strive to make your suspending functions cancellable. A suspending function can be made of several suspending functions. All of them should be cancellable.
try { someWork() } catch (e: Throwable) { if (e is CancellationException) { throw e } ... }
Thank you for reading!
This article was previously published on proandroiddev.com