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 tocoroutineContextreceiver 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 myFuncManuel 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)