Reputation: 189
I thought that a coroutine would stop execution once being in a suspension point (here delay
), but for my surprise it continues anyway. Shouldn't it stop regardless of context?
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob1 = Job()
val scope1 = CoroutineScope(parentJob1)
val parentJob2 = Job()
val scope2 = CoroutineScope(parentJob2)
val job1 = scope1.launch {
println("Coroutine in scope1 started")
withContext(scope2.coroutineContext) {
println("Switched to scope2's context")
delay(1000)
println("This is supposed to not print after cancelling")
}
println("Coroutine in scope1 terminated")
}
delay(500) // Wait a bit
job1.cancel() // Cancel the coroutine
job1.join()
println("Coroutine cancelled")
}
The output is
Coroutine in scope1 started
Switched to scope2's context
This is supposed to not print after cancelling
Coroutine in scope1 terminated
Coroutine cancelled
I would like to add two observations (i) if we were to change job1.cancel()
to parentJob1.cancel()
, the observed behavior would be the same and (ii) if we put a suspension point right after the withContext
block, the coroutine stops.
Upvotes: 1
Views: 70
Reputation: 37799
First, a word of warning: you're hacking structured concurrency by manually creating jobs and custom scopes, this is advanced stuff, not basic coroutines. If you're not familiar with this, you should probably start with a more natural/idiomatic setup, which is also simpler. It's quite rare to need to create jobs manually.
The reason the cancellation doesn't work in your case is that you cancel job1
, but your delay is executed in parentJob2
. Indeed, scope2.coroutineContext
contains the job of scope2
, which is parentJob2
. By passing a job to withContext
, you break the relationship of the coroutine with its parent, and you didn't give any parent to parentJob2
. so you can't rely on the safe world of structured concurrency anymore (cancellations of job1
will not propagate inside withContext
here).
Upvotes: 5
Reputation: 17288
This is because withContext
is merging the current and provided coroutine context.
Most commonly this is done just to switch dispatchers, eg. withContext(Dispatchers.IO)
in which case the block inherits the Job
and becomes a child of it, behaving as you'd expect.
However by providing a context with separate Job
you don't establish a relationship. Provided block will run independent of context that called it (so even when it gets cancelled).
Also withContext
will only check for cancellation of merged context, so that's why you can see it "fall through" to your "scope1 terminated" print.
Upvotes: 1