Coroutines: different ways to change coroutine context

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 :slight_smile:

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 :slight_smile: 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 :slight_smile: 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 :wink:

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 :wink:

Trying to derive this higher level functionis purley for educational proposes of course.

Thanks @nickallendev for all your help! :slight_smile: