Hi, I'm trying to implement a `race` function that...
# coroutines
l
Hi, I'm trying to implement a
race
function that would take a list of
Deferred
, await for the single fastest (biased or not, doesn't matter), cancel all the "losers", and return the value of the winner. Did someone already solve this problem, or has a clue on how to make this? Thank you!
g
not sure about it but can you do a
Select
on a list of deferred?
1
l
Doesn't seem to be provided out of the box.
And to be honest, I understand close to everything in kotlinx.coroutines… but that
select
thing. I still didn't succeed in wrapping my head around it, and consequently I don't use it…
g
my understanding is that
select
allows you to wait on multiple coroutines at the same time and give you back the result of the first one that completes. If there was a version of it that gets a list of suspending functions, in this case
Deferred<T>::await
, you can get the result of the first one and then cancel all coroutines in the list with a
forEach { it.cancel() }
. But I’m not sure this is possible and, more importantly, how exceptions need to be handled
a
The example in the documentation does more or less the thing you need: https://kotlinlang.org/docs/reference/coroutines/select-expression.html#selecting-deferred-values
b
Copy code
suspend fun <T> List<Deferred<T>>.race(): T = select {
    forEach {
        it.onAwait {
            it
        }
    }
}
a
I think, that one, won't cancel all others, but it could be solved by launching all those deferred in a supervisor scope and canceling it.
l
No need for a supervisorScope there
b
Copy code
suspend fun <T> List<Deferred<T>>.race(): T = select {
    forEach {
        it.onAwait { result ->
            forEach { other -> 
                if (other != it) {
                    other.cancel()
                }
            }
            result
        }
    }
}
👍 1
l
Thanks for the hints for the
select
technique! I'll make a version that supports cancellation with it copy paste @bdawg.io snippet, and try to benchmark it against my initial implementation when I'm back on a computer.
b
There’s no need for the
other != it
part. You can invoke
cancel
on a completed job
👍 1
you could clean it up too if you want to use another extension
Copy code
fun <T, C : Iterable<T>> C.cancelAll() = onEach { it.cancel() }
👍 1
l
It's late there, I'll benchmark this tomorrow:
Copy code
private suspend fun <T> List<Deferred<T>>.raceWithSelect(): T = select {
    forEach { it.onAwait { result -> forEach(Deferred<T>::cancel); result } }
}
a
@louiscad Is the
Deferred
required? Because an alternative could be to send the winning race to a channel or callback, which could cancel a job, some psuedo code:
Copy code
val racingJob = Job()
val racingActor = actor<Race> {
	val winningRace = channel.receive()
	racingJob.cancel()
	
	// Do something with winninRace
}

repeat(10) {
	launch(racingJob) {
		racingActor.send(Race())
	}
}
This way you don't have to manage the jobs independently
l
@Albert
Deferred
makes it easy to get the value (if winning), or be cancelled otherwise. Using a channel or an actor is not natural for my use case which is awaiting for the user to make an action on the user interface in front of a choice and get a value associated with the choice made when resuming the coroutine. I added the following top level function relying on the one I posted above:
Copy code
suspend inline fun <T> raceOf(vararg racers: Deferred<T>): T = racers.asList().raceWithSelect()
And I use it this way:
Copy code
val someValue: SomeType = raceOf(
    async {
        firstButton.awaitOneClick(hideAfterClick = true) // Cancellable and finally hides the View
        firstValue
    },
    async {
        secondButton.awaitOneClick(hideAfterClick = true) // Cancellable and finally hides the View
        secondValue
    }
)
a
If needed you could still solve it with actor:
Copy code
val winningValue = CompletableDeferred<T>()
val racingActor = actor<Race> {
    val winningRace = channel.receive()
    racingJob.cancel()
    winningValue.complete(winningRace)
}
But I could imagine it is still unnatural for the type application you are writing.
b
Any type of channel is probably overkill when only one value will ever be returned (aka, a `Deferred`/`Promise`/`Single`)