https://kotlinlang.org logo
Title
c

Colton Idle

05/12/2023, 4:04 PM
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

streetsofboston

05/12/2023, 4:06 PM
// inside `suspend` fun
...
   coroutineScope { ... here goes your launch/async ... }
...
c

Colton Idle

05/12/2023, 4:08 PM
Thanks for the pointer @streetsofboston!
y

yschimke

05/12/2023, 4:12 PM
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

Francesc

05/12/2023, 4:29 PM
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

Casey Brooks

05/12/2023, 4:30 PM
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

Francesc

05/12/2023, 4:31 PM
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

Casey Brooks

05/12/2023, 4:35 PM
For example, this is what you probably shouldn’t do:
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:
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

Francesc

05/12/2023, 4:38 PM
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

Casey Brooks

05/12/2023, 4:47 PM
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

Francesc

05/12/2023, 4:48 PM
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

Casey Brooks

05/12/2023, 4:57 PM
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

Francesc

05/12/2023, 6:27 PM
it is different to have a function that does
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

Patrick Steiger

05/13/2023, 3:03 AM
It’s either
fun CoroutineScope.someAsyncFun()
or
suspend fun someFun()
Having it both suspend and take a scope is confusing
f

Francesc

05/13/2023, 3:38 PM
it's actually 3 options,
fun CoroutineScope.someWorkAsync()
fun someWorkAsync(scope: CoroutineScope)
suspend fun someWorkThatCompletsWhenWeReturn()
p

Patrick Steiger

05/13/2023, 3:39 PM
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

Francesc

05/13/2023, 3:40 PM
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

Chris Fillmore

05/13/2023, 4:11 PM
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