If I'm in a suspend function already, and I have t...
# coroutines
c
If I'm in a suspend function already, and I have two other internal suspend functions that I want that function to call in parrallel (fire and forget). What can I do? I thought launch {} would work, but apparently I can't call that in a suspend function?
s
Copy code
// inside `suspend` fun
...
   coroutineScope { ... here goes your launch/async ... }
...
c
Thanks for the pointer @streetsofboston!
y
That's always felt unintuitive to me, is it because coroutineScope surrounds a bunch of possible coroutines that must all exit for it to complete? While a suspend function only needs to get to the end or return?
f
a
coroutineScope
will wait for the coroutines inside to complete before returning, which is correct. You should not "fire and forget" from a
suspend
function because that breaks convention, a
suspend
function should only return once it has completed its work; if it launches a coroutine and returns, then it hasn't completed its work before returning
c
The
coroutineScope { }
block will wait for the jobs launched inside it to complete before returning, so it’s not perfectly suitable for a “fire-and-forget” scenario if you need the main coroutine to continue executing beyond that block. If you want to truly fire-and-forget, you should create a new
CoroutineScope
and launch into that (making sure to hold onto it somewhere that you can cancel at the appropriate point in your app’s lifecycle). This is actually an example of Structured Concurrency in action. The Coroutines APIs force you to be aware of where you’re launching jobs so they’re tracked properly. You must decide whether to use
coroutineScope { }
, which ties those jobs to the current coroutine, or launch them onto a separate
CoroutineScope
object and make it that scope’s job to handle cancellation and errors.
f
if you want to launch a coroutine, then either make your function an extension on
CoroutineScope
or pass the scope as a parameter (and, in both cases, the function is not suspending)
c
For example, this is what you probably shouldn’t do:
Copy code
class MyRepository {
    suspend fun doSomeWork() { 
        delay(1000) // do some initial work

        // don't do this, you'll get stuck in the coroutineScope block longer than you want
        coroutineScope { 
            launch { /* some other work*/ }
            launch { /* some other work*/ }
        }
        
        // continue with other work
    }
}
And here’s what you probably want to do instead:
Copy code
class MyRepository {
    private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    public fun close() {
        coroutineScope.cancel()
    }

    suspend fun doSomeWork() {
        delay(1000) // do some initial work

        // do this instead. By referencing another CoroutineScope, it's clear these jobs are not managed by doSomeWork()
        coroutineScope.launch { /* some other work*/ }
        coroutineScope.launch { /* some other work*/ }

        // continue with other work
    }
}
f
that is not the recommendation
If you need to launch a coroutine that keeps running after your function returns, then make your function an extension of
CoroutineScope
or pass
scope: CoroutineScope
as parameter to make your intent clear in your function signature. Do not make these functions suspending:
https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055
this part is also important
Suspending functions, on the other hand, are designed to be non-blocking and should not have side-effects of launching any concurrent work. Suspending functions can and should wait for all their work to complete before returning to the caller³.
c
That’s just a syntactic thing. It doesn’t change the fact that you should be explicitly launching into a
CoroutineScope
, but you need to define what what scope is.
suspend fun
itself does not have a
CoroutineScope
receiver to launch into, so you need to find a way to access it. The two options are either opening a
coroutineScope { }
block and suspending until its children complete, or launching onto some other scope. Note that in the above article, it’s referening the
runBlocking
function, and like
launch { }
, and
async { }
the lambda has a
CoroutineScope
receiver because they are considered “top-level coroutine builders”. You’re supposed to just launch other jobs inside those builders. But a
suspend
function is running inside one of those builders, and is not a top-level coroutine itself. It’s supposed to suspend until it is completely finished with everything it needs. so if you want to do something like launch fire-and-forget tasks you need to do it in relation to the scope it’s currently running in. You’re not supposed to have jobs still running on the parent’s coroutineScope once the
suspend
function returns, which is why you need to move them to another coroutineScope if you want to fire-and-forget
f
it's not a syntactic thing, it's a convention that should be respected, we should not be launching coroutines that keep running after a
suspend
function has returned
c
Yes, it’s true that a pattern of fire-and-forget is typically considered an anti-pattern. That said, Channels, Actors, and other concurrency patterns exist because there are many legitimate use-cases for such fire-and-forget type work, and these APIs depend on being able to move work between coroutines without suspending the sender. The best pattern is to not do fire-and-forget, and try to structure your app logic appropriately. But there are ways to do properly within the rules of Structured Concurrency if that’s what your app actually needs
f
it is different to have a function that does
Copy code
fun myFunction() {
   // some stuff here
   // other stuff there
   scope.lauch { channel.send(someValue) }
}
than to have a
suspend
function that launches coroutines that continue to run after the function returns, as that breaks the convention that a
suspend
function should not return until all work is complete
p
It’s either
fun CoroutineScope.someAsyncFun()
or
suspend fun someFun()
Having it both suspend and take a scope is confusing
f
it's actually 3 options,
Copy code
fun CoroutineScope.someWorkAsync()
fun someWorkAsync(scope: CoroutineScope)
suspend fun someWorkThatCompletsWhenWeReturn()
p
I only do (2) when the fun is already an extension on something else, so we can’t have CoroutineScope as receiver (until context receiver stabilizes)
f
the extension function works when it's a member function, but that gets cumbersome if it's a public method, in which case I prefer to pass the scope as argument. But I tend not to use these approaches and use
suspend
functions instead, I think the code is easier to read and understand that way; I use the extension function or passing a
scope
in limited cases
c
Another approach to consider would be to do your suspending work in your function, but emit your side effects (fire and forget) into a Flow and collect on the Flow. If the Flow is buffered (and has room) then this will be the same as if you had launched a coroutine