Manuel Pérez Alcolea
04/28/2023, 3:48 AMCoroutineScope
with a suspend
method that calls any fun CoroutineScope.function()
...
class MyClass: CoroutineScope {
override val coroutineContext = SupervisorJob()
suspend fun myFunc() {
someSuspendFunction()
launch { } // warning! Ambiguous coroutineContext!
}
}
I get the warning
AmbiguousI don't understand what's ambiguous about it? `myFunc`'s context should be defined by... wherever it's called from, and the block's context fordue tocoroutineContext
receiver of suspend functionCoroutineScope
launch
should be defined by this
(an instance of MyClass
), shouldn't it?
I also don't understand why adding a run
removes the warning
run {
launch { }
}
or calling another method that calls CoroutineScope.launch()
fun myLauncher() = launch { }
// ...
suspend fun myFunc() {
someSuspendFunction()
launch { }
}
I've read that extending CoroutineScope
is often avoided, and that's fine, but I'd still want to know what's going on here. I also came across a blog post by Roman Elizarov talking about Explicit concurrency, but I want to launch-and-forget coroutines right after calling a suspend function.Casey Brooks
04/28/2023, 3:02 PMlaunched
coroutine inherits from its parent context, and thus cancellation can error handling is able to follow that hierarchy naturally.
So when looking at this code, you need to follow the coroutine hierarchy, and you’ll notice that it’s not doing this. myFunc
is marked suspend
, which means that it is going to be running on the CoroutineScope
of whoever calls that function. The “natural parent” of myFunc
is not MyClass
, but is the coroutine that called instanceOfMyClass.myFunc()
.
With that in mind, when you try to run launch
within myFunc, it needs to be launched on a CoroutineScope
. Its signature is CoroutineScope.launch
, but this is more than simply needing any CoroutineScope
as the receiver. The expectation is that, since your calling it from a suspend
function, that the launch
function is related to the parent CoroutineScope of the suspend function. But in this case it is not. Since MyClass
implements CoroutineScope
and overrides the coroutineContext
, launch
now has MyClass
as a receiver with which to launch upon. But MyClass
is not related to the parent CoroutineScope of myFunc
in any way, so it would not be able to cooperate with cancellation or anything else.
And this is why you’re getting a warning. It technically would be able to launch, but it’s pretty clear that this is a bad pattern. When one reads the code, they would expect it to do one thing (be related to myFunc
) but in reality it’s just running on it’s own. Because the class implements CoroutineScope
, much of this logic becomes implicit and hard to follow. This warning comes because this is a common antipattern, of attempting to launch
from a suspend
function. But when you wrap it in run
, as far as the compiler sees, that launch
block is no longer directly within a suspend
function, so the warning goes away. But that doesn’t mean that it fixes the problem.
So like you mentioned, a better way to go to launch a “fire-and-forget” task as the end of myFunc()
is to be explicit about it. Don’t implement CoroutineScope
, but instead hold onto a coroutineScope
property so that you’re forced to do coroutineScope.launch
, which makes it explicit that the launched code is not related to myFunc
Manuel Pérez Alcolea
04/28/2023, 11:05 PMprivate
scope is also an anti pattern? I like how jobs propagate exceptions and also how you can cancel an entire scope, it makes everything work tightly as long as every part is willing to cooperate... but I also think there might be cases where I might want to have a less-flexible library where some `Job`s are not in reach at all (and the user might need to call my custom .stop()
or whatever to cancel the scope and everything else.) Mostly out of laziness, though, because these `Job`s be designed to catch cancellations and clean up (or callback to clean up) by themselves
(when I say "clean up" I mean, in my case, I'm actually keeping track of these jobs, through a map and a few methods. My state is a bit all over the place)