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

louiscad

12/26/2018, 5:38 PM
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

gmariotti

12/26/2018, 5:47 PM
not sure about it but can you do a
Select
on a list of deferred?
1
l

louiscad

12/26/2018, 5:51 PM
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

gmariotti

12/26/2018, 6:04 PM
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

altavir

12/26/2018, 6:31 PM
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

bdawg.io

12/26/2018, 6:32 PM
Copy code
suspend fun <T> List<Deferred<T>>.race(): T = select {
    forEach {
        it.onAwait {
            it
        }
    }
}
a

altavir

12/26/2018, 6:33 PM
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

louiscad

12/26/2018, 6:35 PM
No need for a supervisorScope there
b

bdawg.io

12/26/2018, 6:35 PM
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

louiscad

12/26/2018, 6:36 PM
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

bdawg.io

12/26/2018, 6:37 PM
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

louiscad

12/26/2018, 10:30 PM
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

Albert

12/27/2018, 8:18 PM
@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

louiscad

12/27/2018, 8:42 PM
@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

Albert

12/27/2018, 9:10 PM
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

bdawg.io

12/27/2018, 10:16 PM
Any type of channel is probably overkill when only one value will ever be returned (aka, a `Deferred`/`Promise`/`Single`)
3 Views