I have a question. First code snippet: ```fun ma...
# coroutines
n
I have a question. First code snippet:
Copy code
fun main(): Unit = runBlocking {
    val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO> + SupervisorJob())

    scope.launch {
        delay(1000)
        throw Error("Child 1 failed")
    }

    scope.launch {
        delay(1500)
        println("Child 2 Success")
    }

    // Wait
    delay(3000)
}
Output:
Copy code
Exception in thread "DefaultDispatcher-worker-3 @coroutine#2" java.lang.Error: Child 1 failed
 //stacktrace

Child 2 Success
Observation: Child 2 is not canceled, the program continues running normally and just logs the stack trace. Second code snippet:
Copy code
viewModelScope.launch {
    delay(1000)
    throw Error("Child 1 failed")
}

viewModelScope.launch {
    delay(1500)
    println("Child 2 Success")
}
Output:
Copy code
FATAL EXCEPTION: main
Process: com.example.myapplication, PID: 6877
java.lang.Error: Child 1 failed
 at...
Observation: The line
"Child 2 Success"
is not printed. The application crashes, terminating the whole process. I understand that in the second code snippet, the exception propagates to the parent coroutine. Since Android’s default exception handler crashes the app, the whole process is terminated. Does this mean that, by default in Android, we do not benefit from
SupervisorJob
at all? We still need to handle exceptions using
CoroutineExceptionHandler
or a
try-catch
block? Kotlin Coroutine documentation provides examples that are suitable for regular Java/Kotlin programs, but they don’t apply the same way in Android. This can be misleading and confusing for developers.
d
Does this mean that, by default in Android, we do not benefit from
SupervisorJob
at all?
It does not mean that.
SupervisorJob
can ensure that coroutines won't get cancelled if other coroutines in the same scope fail. Whether the program itself will crash is another question.
We still need to handle exceptions using
CoroutineExceptionHandler
or a
try-catch
block?
Yes, if a coroutine is `launch`'ed (instead of being created with
async
), it will attempt to notify its parent about the failure, but if the parent is not interested (that is, is a supervisor), then it will use the
CoroutineExceptionHandler
, or if there is none, the default one for the platform.
Kotlin Coroutine documentation provides examples that are suitable for regular Java/Kotlin programs
Could you please provide links to these examples?
l
This line:
val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO> + SupervisorJob())
Could lead to coroutines being garbage collected while suspended. You want to keep the scope in a member or top-level strong reference, and you might just need to create a local scope with
supervisorScope { … }
if you want to not rethrow child exceptions/throwables.
d
Could lead to coroutines being garbage collected while suspended.
I think you're talking about https://github.com/Kotlin/kotlinx.coroutines/issues/1061, but for this to be a problem, you need someone who would want to potentially wake up a coroutine to store it as a weak reference.
l
Hum, you just need bad luck, actually. If you take Android's MediaPlayer API and make a suspending extension on it. The callbacks are stored in the
MediaPlayer
instance, which is only a local property (so only a cycle reference between the
ContinuationImpl
and
MediaPlayer
), and the system keeps the
MediaPlayer
instance as a weak reference, so the callback that references the continuation has no stable root, and when the GC comes, only a strongle referenced
Job
(via a strongly referenced scope) will prevent it from disappearing silently while suspended. Any API can do this kind of thing, directly, or indirectly. The best thing to do is to make sure any custom
CoroutineScope
is strongly referenced somewhere, so you don't need to check all the suspending functions that might be called within that scope (which you might not control!)
That's also a good reason to move forward and deprecate
GlobalScope
(since it's never keeping any reference to the child coroutines, effectively making them all orphans, potentially subject to kidnapping by the GC)
d
Any API can do this kind of thing, directly, or indirectly.
That also means you can't directly pass lambdas/anonymous classes to this API at all, as those are not strongly referenced, which makes the API error-prone. Every user of
MediaPlayer
has to be aware of this, even if coroutines manage to prevent some portion of the problems. I believe the cleaner solution is to work around this on the call site of the problematic API, regardless of whether coroutines are used at all: https://github.com/Kotlin/kotlinx.coroutines/issues/1061#issuecomment-2796523272
l
Do you really expect every user of MediaPlayer to know about this, and develop a workaround at the API wrapper site?
d
I expect most users of
MediaPlayer
to inevitably encounter this issue eventually. Having every user of coroutines pay the price of measurably increased memory consumption for shielding the users of
MediaPlayer
some of the time (and not even needing our protection any more after learning the gotcha) doesn't feel feasible to me.
l
How is keeping strong references to Scopes/Jobs that are still alive relate to increasing memory consumption? Because there are 4 bytes to keep the reference?
d
It's not the bytes for storing the reference that's the problem but the reference itself. For example, the whole coroutine without a parent that was waiting for a
SharedFlow
update gets GC'd if the
SharedFlow
itself gets GC'd.
l
Of course, that's expected
I meant that just making and forgetting a CoroutineScope is error prone
d
Ah, then I completely misunderstood your point, sorry! Yes, absolutely, manually calling
CoroutineScope()
inside a function is error-prone: coroutine scopes created like this are purely for storing in object fields and should be cancelled when the object is no longer needed.
👌🏼 1
🙂 1