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()
}
}
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:
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.
I deleted your dispose()
function, we have cancel()
out-of-the-box.
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:
added a delay
into each coroutine and a println
to see when it's done.
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) {