https://kotlinlang.org logo
#coroutines
Title
# coroutines
b

bbaldino

11/30/2020, 5:41 PM
I've got a use case that I thought would be a good fit for structured concurrency, but I'm struggling with it and not sure if I'm missing something or trying to force a square peg into a round hole. I've got basically a class/job hiearchy, but need the following behavior: 1. If any sub-task errors, then the parent should know so it can cancel the others (this works well with structured concurrency) 2. If any sub-task finishes, then the parent should know so it can stop all the others (This has been one of the trickier parts to get working with structured concurrency) 3. Stopping the parent should stop all the children (this works well with structured concurrency) 4. Tasks need to perform non-trivial cleanup upon completion, both in the 'clean' finish and error modes, but I want to be able to do this on a class level, not just a 'task' level. That is: a class may launch several child jobs, but I don't want to run the cleanup on the completion of each of those child jobs, more on the class' "scope" level. (I even played with writing a
CoroutingScope#onFinish
helper which delayed wrapped an indefinite delay in a try/finally and called a cleanup method in the
finally
block, but it feels wrong). Of these, I think #2 is the biggest sticking point: I find myself wishing I could create a child coroutine scope but wait for any child to finish, instead of all children to finish (like
coroutineScope
and
runBlocking
do). Does that exist?
l

louiscad

11/30/2020, 6:16 PM
raceOf
from Splitties coroutines might be what you're looking for. For the part 4, it depends on where you put the cleanup code (likely in a finally block).
m

marstran

11/30/2020, 6:16 PM
I think you can achieve this using channels. I did a test using
launch
inside
channelFlow
right now, and it seems to act the way you want. I used
first()
on the resulting flow to get the first element to finish. This is what I did:
Copy code
val tasks = // List of tasks

val parent = launch {
    channelFlow {
        tasks.forEach { task ->
            launch {
                send(task.run())
            }
        }
    }.first()
}

parent.invokeOnCompletion { 
    println("Cleanup") 
}
b

bbaldino

11/30/2020, 6:17 PM
Thanks @louiscad, I'll take a look at
raceOf
. I did do work in a finally block, but since it blocks the other coroutines from starting cleanup it wasn't a great fit. Not sure the best way to get a different behavior there.
Oh, interesting @marstran...I'll have to mess with that a bit to better understand it and see how it works in the error case.
l

louiscad

11/30/2020, 6:19 PM
You can launch the cleanup in another scope (e.g. a custom one or
GlobalScope
) if the temporary leak is not an issue. @bbaldino
m

marstran

11/30/2020, 6:19 PM
Yeah. The
channelFlow
will automatically cancel all child-jobs when
first()
returns. If something fails first, then that exception will bubble up and cause all the others to be cancelled as well.
b

bbaldino

11/30/2020, 6:20 PM
@louiscad ok, I'll have to play with that.
m

marstran

11/30/2020, 6:20 PM
I guess you can use
supervisorScope
inside your parent if you want more control of the error-handling.
b

bbaldino

11/30/2020, 6:21 PM
Ah good point, I'd been avoiding it since I do want everything to shutdown when one child fails, but I didn't think of using it as a hook to do the cleanup.
l

louiscad

11/30/2020, 6:23 PM
@bbaldino Here's the link to the doc of the
raceOf
function you can use via the depends or copy pasting: https://github.com/LouisCAD/Splitties/tree/main/modules/coroutines#racing-coroutines
👍 1
b

bbaldino

11/30/2020, 6:44 PM
@marstran what would be the best method if tasks can have child tasks of their own? I'd need to have the parent pass in a CoroutineScope when creating the tasks so they'd have access to it?
m

marstran

11/30/2020, 6:47 PM
You can pass the scope created by
launch
to the task, so it can create more child-tasks on it.
Copy code
launch { task.run(this) }
b

bbaldino

11/30/2020, 6:48 PM
👍
l

louiscad

11/30/2020, 6:50 PM
Usually, there's no need to explicitly pass the
CoroutineScope
as it's the receiver
b

bbaldino

11/30/2020, 6:52 PM
Well, the way my code is set up is that the 'tasks' are really more first-class components which manage some state as well as launch some tasks. So it's a little bit more than just a simple task method, and I don't think having the member method be an extension of coroutinescope will work there? That would require the multiple receiver stuff, I think(?)
l

louiscad

11/30/2020, 6:54 PM
Maybe you can just make these functions suspending so they don't need a scope as a receiver (and use
coroutineScope { }
if they have child coroutines to launch)
b

bbaldino

11/30/2020, 6:54 PM
Yeah, I thought about that but run into the "finish on the first one succeeding" issue with
coroutineScope
But, like you said, maybe a `raceOf`/similar helper will work there.
👍 1
2 Views