问题
I have seen tutorials passing SupervisorJob
to CoroutineScope
to avoid all coroutine jobs being cancelled if one of its child fails.
In run3
, I thought passing SupervisorJob
to launch
can get the same result, but apparently it does not. It seems to allow the Coroutine to be reused if there is an exception (if you remove the SupervisorJob
from launch
, second run2
call won't run the coroutine job), but it dose not behave like supervisorScope
, whose other children job can continue (in the example the first test1.run
call) . I wonder in what scenario we can use this way? Because it looks legit to pass it to launch
constructor.
package coroutine.exceptions
import kotlinx.coroutines.*
fun log(msg: String) = println("$msg (${Thread.currentThread().name})")
val logExceptionHandler = CoroutineExceptionHandler { _, e ->
log(e.localizedMessage)
}
fun main() = runBlocking {
TestReuseCoroutineAfterException4("test1").run {
run1(true)
delay(100)
println()
run1(false)
delay(100)
}
println("================================================================")
TestReuseCoroutineAfterException4("test2").run {
run2(true)
delay(100)
println()
run2(false)
delay(100)
}
println("================================================================")
TestReuseCoroutineAfterException4("test3").run {
run3(true)
delay(100)
println()
run3(false)
delay(100)
println()
}
log("finished")
}
class TestReuseCoroutineAfterException4(
private val testName: String
) : CoroutineScope by CoroutineScope(CoroutineName(testName)) {
// by passing a Job, we can let the exception propagate to this coroutine scope instead of the
// root one, which allows us to reuse the root scope.
fun run1(throwException: Boolean) = launch(logExceptionHandler + Job()) {
val logPrefix = "$testName.run1:"
coroutineScope {
launch {
launch {
if (throwException)
throw RuntimeException("$logPrefix throw exception")
else
log("$logPrefix done (job#1-1)")
}.join()
launch {
log("$logPrefix done (job#1-2)")
}.join()
log("$logPrefix done (job#1)")
}.join()
launch {
log("$logPrefix done (job#2)")
}.join()
}
}
suspend fun run2(throwException: Boolean) {
val logPrefix = "$testName.run2:"
supervisorScope {
launch(logExceptionHandler) {
launch {
if (throwException)
throw Exception("$logPrefix throw exception")
else
log("$logPrefix done (job#1-1)")
}.join()
launch {
log("$logPrefix done (job#1-2)")
}.join()
log("$logPrefix done (job#1)")
}.join()
// this will be run.
launch {
log("$logPrefix done (job#2)")
}.join()
}
}
fun run3(throwException: Boolean) {
val logPrefix = "$testName.run3:"
launch(logExceptionHandler + SupervisorJob()) {
launch {
launch {
if (throwException)
throw Exception("$logPrefix throw exception")
else
log("$logPrefix done (job#1-1)")
}.join()
launch {
log("$logPrefix done (job#1-2)")
}.join()
log("$logPrefix done (job#1)")
}.join()
// this will still be run.
launch {
log("$logPrefix done (job#2)")
}.join()
}
}
}
output
test1.run1: throw exception (DefaultDispatcher-worker-2 @test1#2)
test1.run1: done (job#1-1) (DefaultDispatcher-worker-2 @test1#7)
test1.run1: done (job#1-2) (DefaultDispatcher-worker-2 @test1#8)
test1.run1: done (job#1) (DefaultDispatcher-worker-2 @test1#6)
test1.run1: done (job#2) (DefaultDispatcher-worker-2 @test1#9)
================================================================
test2.run2: throw exception (main @coroutine#10)
test2.run2: done (job#2) (main @coroutine#12)
test2.run2: done (job#1-1) (main @coroutine#14)
test2.run2: done (job#1-2) (main @coroutine#15)
test2.run2: done (job#1) (main @coroutine#13)
test2.run2: done (job#2) (main @coroutine#16)
================================================================
test3.run3: throw exception (DefaultDispatcher-worker-2 @test3#18)
test3.run3: done (job#1-1) (DefaultDispatcher-worker-4 @test3#22)
test3.run3: done (job#1-2) (DefaultDispatcher-worker-4 @test3#23)
test3.run3: done (job#1) (DefaultDispatcher-worker-4 @test3#21)
test3.run3: done (job#2) (DefaultDispatcher-worker-4 @test3#24)
finished (main @coroutine#1)
Process finished with exit code 0
回答1:
if you remove the
SupervisorJob
fromlaunch
, secondrun2
call won't run the coroutine job
The reason for this behavior is not that you are passing a SupervisorJob
, but that you are passing any kind of Job
to it. Try replacing + SupervisorJob()
with + Job()
and the second invocation of run2()
will execute the coroutines.
The main difference is that, when you pass an explicit job to launch
, it becomes the parent job of the launched coroutine instead of the master job within TestReuseCoroutineAfterException4
. Coroutine failure therefore doesn't cancel the master job and the effects are localized to a single invocation.
Passing a job directly to launch
is not an encouraged practice because it breaks structured concurrency and creates the weird semantics that you experienced.
来源:https://stackoverflow.com/questions/60026116/when-do-we-use-launchsupervisorjob