CoroutineScope with SupervisorJob vs supervisorScope

2.6k Views Asked by At

Recently I've been learning coroutines in detail, as I understand SupervisorJob() gives us opportunity to not cancel all children if one of the children of coroutine is cancelled due to some reason.

It is said that coroutines started with coroutineScope will cancel all children if one fails, but the ones started with supervisorScope will only cancel the child which is failed

I wonder if I could change behaviour of CoroutineScope by adding SupervisorJob as CoroutineContext to it, but I couldn't get expected behaviour which is the thing I don't get

Expected behaviour - getData1() and getData3() result gets printed*

Actual: - getData2() cancels all coroutine

fun main() = runBlocking {

    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println(throwable)
    }

    val customScope = CoroutineScope(SupervisorJob() + exceptionHandler)


    customScope.launch {
        launch {
            getData1().also { println(it) }
        }
        launch {
            getData2().also { println(it) }
        }
        launch {
            getData3().also { println(it) }
        }
     }.join()

}


private suspend fun getData1(): String? {
    delay(1000)
    return "data 1"
}

private suspend fun getData2(): String? {
    delay(300)
    throw RuntimeException("While getting Data 2 exception happened")
}

private suspend fun getData3(): String? {
    delay(800)
    return "data 3"
}
4

There are 4 best solutions below

5
On

You are not launching the three inner coroutines in customScope. You're launching them in the scope of the launched outer coroutine (by using implicit this to launch them). If you launch them from that custom scope that has a SupervisorJob, then it should work:

launch {
    listOf(
        customScope.launch {
            getData1().also { println(it) }
        },
        customScope.launch {
            getData2().also { println(it) }
        },
        customScope.launch {
            getData3().also { println(it) }
        }
    ).joinAll()
}.join()
1
On

the SupervisorJob expected behaviour, happens when we call it with the lunch/async from the root scope .

val job1 =rootscope.lunch{}
val job2 =rootscope.lunch{}
val job3 =rootscope.lunch{}

if one of these jobs failed, it will not affect others, However if we use it as a child its acts like a Job() like your example.

but what if i need child's coroutines behave as "if it lunched from a root scope" ? . like your example, then supervisorScope appear to solve this problem, if u use :

      customScope.launch {
        supervisorScope {
            launch {

                throw Exception()
            }
            launch {
                println("hjkhjk 111")
            }
            launch {
                println("hjkhjk 222")
            }
        }
    }.join()

then every child coroutines runs as rootScope.lunch{...} , and your problem will be solved

0
On

It is said that coroutines started with coroutineScope will cancel all children if one fails, but the ones started with supervisorScope will only cancel the child which is failed

That's right, but what you're trying to do is create only 1 child of supervisorScope.

enter image description here

customScope.launch {
    launch {
        // some work
    }
    launch {
        throw RuntimeException()
    }
    launch {
        // some work
    }
 }

What you actually need is some thing like this:

enter image description here

customScope.launch {
    // some work  
}
customScope.launch {
    throw RuntimeException()
}
customScope.launch {
    // some work    
}

This is how paren-child relationship works when creating new coroutines

The new coroutine creates its own child Job instance (using a job from this context as its parent) and defines its child context as a parent context plus its job:

To understand how this works in details I suggest reading this article.

0
On

The main key difference between SupervisorJob and a regular Job is that the Direct children of a supervisor job can fail independently of each other.


import kotlinx.coroutines.*

fun main() {

    val handler =
        CoroutineExceptionHandler { context, throwable -> println("throwable: $throwable, jobContext: ${context[Job]}") }

    val context = Job() + Dispatchers.Default + handler
    val scope = CoroutineScope(context)
    with(scope) {
        launch {
            delay(300)
            throw Exception("Coroutine 1 failed")
        }

        launch {
            delay(400)
            println("Coroutine 2 finished")
        }

    }
    
    Thread.sleep(3000)
}

If you run this example the second coroutine doesn't finish it's execution and it gets cancelled but if you change the Job with SupervisorJob, the second coroutine will finish it work without a problem. It also explain how supervisorScope works. the Direct children of supervisorScope can fail independently of each other.