Search code examples
kotlin-coroutinescoroutinescope

koltin, what's difference from using CoroutineScope directly and derive the class from CoroutineScope


when launching a coroutine it may just create a CoroutineScope and call launch{} from it -- the doSomething_2(),

or derive the class from CoroutineScope and with the class to launch{}. -- the doSomething_1().

Is there difference between these two, which way is preferred?

class AClass : CoroutineScope {

    override val coroutineContext: CoroutineContext = Dispatchers.Main
    
    var theJob1: Job? = null
    var theJob2: Job? = null
    
    fun doSomething_1() {
        theJob1 = launch(Dispatchers.IO) {
            // ... ...
        }
    }
    
    fun doSomething_2() {
        theJob2 = CoroutineScope(Dispatchers.IO).launch {
            // ... ...
        }
    }
    
    fun dispose() {
        theJob1?.cancel()
        theJob2?.cancel()
    }
}

Solution

  • Is there difference between these two, which way is preferred?

    Yes, there's a fundamental difference that makes one correct and the other incorrect. It is about structured concurrency: if your AClass is the root object of your "unit of work", whatever that may be, and is in charge (or the observer) of its lifecycle, then it should also be the root scope for the coroutines you'll launch within it. When the lifecycle ends, AClass should propagate that event to the coroutine subsystem by calling cancel on itself, cancelling the root scope. CoroutineScope.cancel is an extension function.

    I took your code and made the following fixes:

    1. CoroutineScope.coroutineContext must have a Job() inside it so I added it. I removed the dispatcher because it's not relevant to this story and the Main dispatcher is for a GUI, while we're running a simple test.

    2. I deleted your dispose() function, we have cancel() out-of-the-box.

    3. I deleted theJob1 and theJob2 fields because they serve no purpose once you start properly using structured concurrency.

    I also added some code that will allow us to observe the behavior:

    1. added a delay into each coroutine and a println to see when it's done.

    2. added a main function to test it. The function blocks forever at the last line so that we can see what the launched coroutines will do.

    Here's the code:

    import kotlinx.coroutines.*
    import java.lang.Thread.currentThread
    import kotlin.coroutines.CoroutineContext
    
    fun main() {
        val a = AClass()
        a.doSomething_1()
        a.doSomething_2()
        a.cancel()
        currentThread().join()
    }
    
    class AClass : CoroutineScope {
    
        override val coroutineContext: CoroutineContext = Job()
    
        fun doSomething_1() {
            launch(Dispatchers.IO) {
                try {
                    delay(10_000)
                } finally {
                    println("theJob1 completing")
                }
            }
        }
    
        fun doSomething_2() {
            CoroutineScope(Dispatchers.IO).launch {
                try {
                    delay(10_000)
                } finally {
                    println("theJob2 completing")
                }
            }
        }
    }
    

    When you run it, you'll see only theJob1 getting completed while theJob2 runs for the full 10 seconds, not obeying the cancel signal.

    This is because the construct CoroutineScope(Dispatchers.IO) creates a standalone scope instead of becoming the child of your AClass scope, breaking the coroutine hierarchy.

    You could, theoretically, still use the explicit CoroutineScope constructor to keep the hierarchy, but then you'd have something that's clearly not the preferred way:

    CoroutineScope(coroutineContext + Dispatchers.IO).launch {
    

    This would be equivalent to just

    launch(Dispatchers.IO) {