Coroutine Context

Hi, i am trying to get better understanding how the jobs and scope work when the parent job get cancelled. I am little confused by the behaviour i am observing on the below code. I was creating new scope in 3 different ways.

Option 1: Just reusing the parent scope
Option 2: Created a new scope, and i expected a new job to be associated for this scope
Option 3: Same as option 2, but just added a dispatcher to the scope

I expected option 2 and option 3 to behave in the same way when the parent job is cancelled. But i am seeing in case of option 2, the view model can still gets executed even after cancellation. And its not the case with option 3.

Can some one help me understand why is that so, my understanding is adding Dispatcher in option 3 would have just changed the thread in which the block of code was running.

Appreciate your inputs on this.

    import kotlinx.coroutines.*
    import kotlin.coroutines.coroutineContext
    import kotlin.random.Random

    fun main() = runBlocking {

        val viewModel = ViewModel(1)
        val job = launch {
            try {
                viewModel.start()
            } finally {
                println("Job1: Ran finally  ${coroutineContext[Job]}")
            }
        }

        delay(450L) // delay a bit
        println("main: ====== Cancelling ====")
        job.cancel() // cancels the job and waits for its completion
        println("main: ======Cancelled =====")

    }

    class ViewModel(val idnx: Int) {
        suspend fun start() {
            try {
                println("ViewModel$idnx: Starting --- ${coroutineContext[Job]}")
                val response = interactor.load(idnx)
                println("ViewModel$idnx: Loading data ${response} --- ${coroutineContext[Job]}")
            } finally {
                println("ViewModel$idnx: Ran finally ${coroutineContext[Job]}")
            }
        }
    }

    val interactor = Interactor()
    class Interactor {

        val repo = Repo()
        private val supervisorScope = CoroutineScope(SupervisorJob())
        suspend fun load(reference: Int): Int {

    //        return coroutineScope { // OPTION 1
            return withContext(supervisorScope.coroutineContext) { // OPTION 2
    //        return withContext(supervisorScope.coroutineContext + Dispatchers.Default) { // OPTION 3
                println("Interactor$reference: ${kotlin.coroutines.coroutineContext[Job]}")
                val responseData = repo.fetch(reference)
                println("Interactor$reference: After ${kotlin.coroutines.coroutineContext[Job]}")

                responseData
            }
        }
    }

    val random = Random(10)

    class Repo {

        suspend fun fetch(reference: Int): Int {
            println("Repo$reference: Fetching in repo ${coroutineContext[Job]}")
            delay(1000L)
            println("Repo$reference: Delaying ${coroutineContext[Job]}")
            delay(500L)
            return random.nextInt(100, 9999)
        }
    }

Interactor's supervisorScope isn’t related to the outer scope, so cancelling the outer Job won’t affect it. That’s why option 2 doesn’t work.

If you want child coroutines of your inner scope to not cancel their siblings, use option 1 but with “return supervisorScope {” instead.

1 Like

Thanks @ebrowne72 for your response. If you look, option 2 and 3 are pretty similar except there is a dispatcher involved in 3. In case of 2 the result from child coroutines is printed whereas its not the case with 3. Why is that difference… Both in option 2 and 3, my understanding is there is a new job attached with that new coroutine context. when the parent job is cancelled why the results of child are ignored in one case and not in other.

The job created in your main function is not the parent of the supervisorScope's job. When you create a new CoroutineScope using its constructor it isn’t connected to any other scope, even if you call the constructor from within another scope. The coroutineScope() function, however, does create a new scope that is a child of the containing scope. That is why option 1 works.

I don’t know why option 3 seems to work. You really should only call withContext() with a dispatcher, not a full context.

1 Like

Thanks again for the prompt response. Yes option 1 is clear. Its just 2 which i am confused. When you say

did you mean option 2 here.

Also would you mind eloborating little more on this point.

No, I meant option 3. I would expect both options to not work. But combining coroutine contexts is a bit of black magic.

Regarding withContext(), its intent is to switch a coroutine’s execution to a different dispatcher. That seems to be what you want, so a simple withContext(Dispatchers.Default) will do the job. The coroutineScope() function is for parallel decomposition, where you want to create multiple child coroutines within the new scope. If you don’t want one child coroutine’s failure to affect its siblings, then use the supervisorScope() function instead.

1 Like

withContext is code is optimized in cases when a CoroutineDispatcher switch is not needed. It’s likely that one implementation (used when it detects different dispatchers) checks if the job is active right before returning and the other (when it detects the same dispatcher) does not.

2 Likes

Thanks @nickallendev, that seems to the case. Thanks for pointing. If there are no dispatcher in the new context, it seems to be make the code run in the same thread. So in option 2, the thread seems to run until it finds a point which is cancellable. In option 3, as it runs in a new dispatcher, the results are ignored.

 // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
        // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            // There are changes in the context, so this thread needs to be updated
            withCoroutineContext(newContext, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
1 Like