The many differnt ways to provide a coroutine context to some block of code are a bit confusing to me. What is the difference between these methods and when should which be used?
We can just create a new CoroutineScope
and use that for launching the new coroutines. The result is as expected. The failing produce
coroutine does not cancel the parent job and the rest of the program succeeds.
with(CoroutineScope(coroutineContext + SupervisorJob())) {
val numbers = produce<Int> { throw IllegalStateException("uupps") }
runCatching { numbers.consumeEach { println(it) } }
}
delay(10)
println("finished - isActive: $isActive")
The same goes for supervisorScope { }
. Is this basically syntax sugar to the first example?
supervisorScope {
val numbers = produce<Int> { throw IllegalStateException("uupps") }
runCatching { numbers.consumeEach { println(it) } }
}
delay(10)
println("finished - isActive: $isActive")
Then it’s possible to directly pass the SupervisorJob()
to the coroutine builders like produce
. The behavior still seems to be the same.
val numbers = produce<Int>(SupervisorJob()) { throw IllegalStateException("uupps") }
runCatching { numbers.consumeEach { println(it) } }
delay(10)
println("finished - isActive: $isActive")
And then there is withContext
, which seems to start the produce
coroutine unsupervised and crashes. Why is this? Can we only use withContext
to change the dispatcher?
withContext(SupervisorJob()) {
val numbers = produce<Int> { throw IllegalStateException("uupps") }
runCatching { numbers.consumeEach { println(it) } }
}
delay(10)
println("finished - isActive: $isActive")
Would be great if someone can help me to learn to distinguish those properly
1 Like
The same goes for supervisorScope { }
. Is this basically syntax sugar to the first example?
Not quite, your first example creates a Job that is not a child of the containing coroutine’s Job. So structured concurrency will not apply (cancellation won’t reach the code inside with). See: https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency
val numbers = produce(SupervisorJob()) { throw IllegalStateException(“uupps”) }
Handing produce the SupervisorJob directly creates a child Job that fails which then does not fail the parent SupervisorJob.
withContext(SupervisorJob()) {
val numbers = produce { throw IllegalStateException(“uupps”) }
The code using withContext creates two Jobs in addition to the SupervisorJob. withContext creates a Job whose parent is the SupervisorJob. produce creates a Job whose parent is the Job created by withContext. The job created by withContext fails when its child Job created by produce fails. Note that the return type of withContext is not Unit, withContext returns a result. Since the withContext coroutine failed, that result is an exception.
The same goes for supervisorScope { }
. Is this basically syntax sugar to the first example?
Not quite, your first example creates a Job that is not a child of the containing coroutine’s Job. So structured concurrency will not apply (cancellation won’t reach the code inside with). See: https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency
Aaah, that’s an eye opener. I somehow assumed that coroutineContext + SupervisorJob()
would set up the job hierarchy. But of course it just replaces the job with the new one!
val numbers = produce(SupervisorJob()) { throw IllegalStateException(“uupps”) }
Handing produce the SupervisorJob directly creates a child Job that fails which then does not fail the parent SupervisorJob.
So I guess the same issue with structured concurrency applies here as well. Canceling the outermost coroutine wouldn’t cancel the produce job, because the directly passed SupervisorJob
has no parent?
The code using withContext creates two Jobs in addition to the SupervisorJob. withContext creates a Job whose parent is the SupervisorJob. produce creates a Job whose parent is the Job created by withContext. The job created by withContext fails when its child Job created by produce fails. Note that the return type of withContext is not Unit, withContext returns a result. Since the withContext coroutine failed, that result is an exception.
So the coroutine created by withContext
fails with the exception, because its Job is canceled by the failing produce
coroutine, which passes the exception to cancel
?
It wasn’t clear to me that withContext
creates another job. Is this information missing in the documentation or is this implicit by some other statement? I thought it would just use the given Job.
Is withContext(coroutineContext){}
semantically the same as coroutineScope{}
? As I understand it, both create a child job of the current job and return the result of given block coroutine.
After some time trying I see now that it is not so trivial to rebuild supervisorJob
by using CoroutineScope(…)
. When the SupervisorJob
gets the parent job, it must be properly completed / canceled. Anyway here is my new attempt Is it closer to the original this time?
suspend fun <T> CoroutineScope.mySupervisorScope(block: suspend CoroutineScope.() -> T): T {
val supervisor = SupervisorJob(coroutineContext[Job])
try {
val result = CoroutineScope(coroutineContext + supervisor).block()
supervisor.complete()
supervisor.join()
return result
} catch (e: Exception) {
supervisor.completeExceptionally(e)
throw e
}
}
Canceling the outermost coroutine wouldn’t cancel the produce job, because the directly passed SupervisorJob
has no parent?
Correct
So the coroutine created by withContext
fails with the exception, because its Job is canceled by the failing produce
coroutine, which passes the exception to cancel
?
Mostly correct, it’s not cancel
, it’s a childCancelled
internal method
It wasn’t clear to me that withContext
creates another job. Is this information missing in the documentation or is this implicit by some other statement?
No, withContext is not clear about this at all (IMO), but CoroutineScope is helpful:
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
Is withContext(coroutineContext){}
semantically the same as coroutineScope{}
?
As far as I’m aware, yes. They are intended for different use cases, docs make purpose of each pretty clear, I think.
Anyway here is my new attempt Is it closer to the original this time
On the surface, supervisorScope is not an extension method, and it handles any Throwable, not just Exception. As far as a being a Structured Concurrency learning exercise, it looks pretty good to me. Let’s stick with what the experts created for us in our real code, though
On the surface, supervisorScope is not an extension method, and it handles any Throwable, not just Exception. As far as a being a Structured Concurrency learning exercise, it looks pretty good to me. Let’s stick with what the experts created for us in our real code, though
Trying to derive this higher level functionis purley for educational proposes of course.
Thanks @nickallendev for all your help!