Archie G. Quiñones
Archie G. Quiñones

Reputation: 13688

When to use coroutineScope vs supervisorScope?

Can someone explain what exactly is the difference between these two?

When do you use one over the other?

Thanks in advance.

Upvotes: 55

Views: 20054

Answers (5)

Marko Topolnik
Marko Topolnik

Reputation: 200196

The best way to explain the difference is to explain the mechanism of coroutineScope. Consider this code:

suspend fun main() = println(compute())

suspend fun compute(): String = coroutineScope {
    val color = async { delay(60_000); "purple" }
    val height = async<Double> { delay(100); throw HttpException() }
    "A %s box %.1f inches tall".format(color.await(), height.await())
}

compute() "fetches" two things from the network (imagine the delays are actually network operations) and combines them into a string description. In this case the first fetch is taking a long time, but succeeds in the end; the second one fails almost right away, after 100 milliseconds.

What behavior would you like for the above code?

  1. Would you like to color.await() for a minute, only to realize that the other network call has long failed?

  2. Or perhaps you'd like the compute() function to realize after 100 ms that one of its network calls has failed and immediately fail itself?

With supervisorScope you're getting 1., with coroutineScope you're getting 2.

The behavior of 2. means that, even though async doesn't itself throw the exception (it just completes the Deferred you got from it), the failure immediately cancels its coroutine, which cancels the parent, which then cancels all the other children.

This behavior can be weird when you're unaware of it. If you go and catch the exception from await(), you'll think you've recovered from it, but you haven't. The entire coroutine scope is still being cancelled. In some cases there's a legitimate reason you don't want it: that's when you'll use supervisorScope.

A Few More Points

Let's make two changes to our program: use supervisorScope as discussed, but also swap the order of awaiting on child coroutines:

suspend fun main() = println(compute())

suspend fun compute(): String = supervisorScope {
    val color = async { delay(60_000); "purple" }
    val height = async<Double> { delay(100); throw HttpException() }
    "The box is %.1f inches tall and it's %s".format(height.await(), color.await())
}

Now we first await on the short-lived, failing height coroutine. When run, this program produces an exception after 100 ms and doesn't seem to await on color at all, even though we are using supervisorScope. This seems to contradict the contract of supervisorScope.

What is actually happening is that height.await() throws the exception as well, an event distinct from the underlying coroutine throwing it.

Since we aren't handling the exception, it escapes from the top-level block of supervisorScope and makes it complete abruptly. This condition — distinct from a child coroutine completing abruptly — makes supervisorScope cancel all its child coroutines, but it still awaits on all of them to complete.

So let's add exception handling around the awaits:

suspend fun compute(): String = supervisorScope {
    val color = async { delay(60_000); "purple" }
    val height = async<Double> { delay(100); throw Exception() }
    try {
        "The box is %.1f inches tall and it's %s".format(height.await(), color.await())
    } catch (e: Exception) {
        "there was an error"
    }
}

Now the program does nothing for 60 seconds, awaiting the completion of color, just as described.

Or, in another variation, let's remove exception handling around awaits, but make the color coroutine handle the CancellationException, wait for 2 seconds, and then complete:

suspend fun compute(): String = coroutineScope {
    val color = async {
        try {
            delay(60_000); "purple"
        } catch (e: CancellationException) {
            withContext(NonCancellable) { delay(2_000) }
            println("color got cancelled")
            "got error"
        }
    }
    val height = async<Double> { delay(100); throw Exception() }
    "The box is %.1f inches tall and it's %s".format(height.await(), color.await())
}

This does nothing for 2.1 seconds, then prints "color got cancelled", and then completes with a top-level exception — proving that the child coroutines are indeed awaited on even when the top-level block crashes.

Upvotes: 99

Lior
Lior

Reputation: 7915

As @N1hk mentioned, if you use async the order of calling await matters. And if you're depending on a result from both asynch..await blocks, then it makes sense to cancel as early as possible so that's not an ideal example for supervisorScope

The difference is more apparent when using launch and join:

fun main() = runBlocking {
    supervisorScope {
        val task1 = launch {
            println("Task 1 started")
            delay(100)
            if (true) throw Exception("Oops!")
            println("Task 1 completed!")
        }
        val task2 = launch {
            println("Task 2 started")
            delay(1000)
            println("Task 2 completed!")
        }

        listOf(task1, task2).joinAll()
        println("Finished waiting for both tasks")
    }

    print("Done!")
}

With supervisorScope, the output would be:

Task 1 started
Task 2 started
Exception in thread "main" java.lang.Exception: Oops!
...
Task 2 completed!
Finished waiting for both tasks
Done!

With coroutineScope the output would be just:

Task 1 started
Task 2 started

Upvotes: 7

Muhammad Zahab
Muhammad Zahab

Reputation: 1105

CoroutineScope -> Cancel whenever any of its children fail.

SupervisorScope -> If we want to continue with the other tasks even when one fails, we go with the supervisorScope. A supervisorScope won’t cancel other children when one of them fails.

Here is a useful link for understanding of coroutine in details:

https://blog.mindorks.com/mastering-kotlin-coroutines-in-android-step-by-step-guide

Upvotes: 5

EMEM
EMEM

Reputation: 3148

coroutineScope -> The scope and all its children fail whenever any of the children fails

supervisorScope -> The scope and all its children do NOT fail whenever any of the children fails

Upvotes: 24

Alexey Soshin
Alexey Soshin

Reputation: 17721

I think Roman Elizarov explain it quite in details, but to make it short:

Coroutines create the following kind of hierarchy:

  • Parent Coroutine
    • Child coroutine 1
    • Child coroutine 2
    • ...
    • Child coroutine N

Assume that "Coroutine i" fails. What do you want to happen with its parent?

If you want for its parent to also fail, use coroutineScope. That's what structured concurrency is all about.

But if you don't want it to fail, for example child was some kind of background task which can be started again, then use supervisorScope.

Upvotes: 52

Related Questions