Custom Kotlin Coroutine Races
Find the fastest, then change the rules to filter out nulls or pick your own winner
Kotlin doesn’t have a built-in race()
function like JavaScript or some other languages, but it only takes one line to write your own.
suspend fun <T> race(vararg tasks: suspend () -> T): T =
channelFlow { tasks.forEach { launch { send(it()) } } }.first()
Use race()
to run several suspending functions at once, get the result from the fastest, and cancel all the slower ones that didn’t make the cut.
Let’s break it down.
- We made a
channelFlow
. - We launched a coroutine for each task, which will
send()
the result to thechannelFlow
when it’s done. - We used
first()
to grab the first item from theFlow
—which will be the result from the first coroutine that managed to complete.
The channelFlow
has its own coroutine scope, so all the coroutines act as linked subtasks of the Flow
. When first()
gets an item, the Flow
collection ends, and the remaining coroutines are automatically cancelled.
Let’s try it.
suspend fun slow(): String {
delay(1000)
return "This function is slow 🐌"
}
suspend fun fast(): String {
delay(10)
return "This function is fast 🚀"
}
suspend fun <T> race(vararg tasks: suspend () -> T): T =
channelFlow { tasks.forEach { launch { send(it()) } } }.first()
suspend fun main() {
val result = race(::slow, ::fast)
println(result) // → "This function is fast 🚀"
}
Run it here: https://pl.kotl.in/ip5x4BevO
Filter out nulls and choose a new winner
What if you don’t just want the fastest task, but the fastest task that returns a non-null value? Easy—swap first()
for first { it != null }
.
suspend fun slow(): String? {
delay(1000)
return "This function is slow 🐌"
}
suspend fun fast(): String? {
delay(10)
return null
}
suspend fun <T> raceNotNull(vararg tasks: suspend () -> T): T =
channelFlow { tasks.forEach { launch { send(it()) } } }.first { it != null }
suspend fun main() {
val result = raceNotNull(::slow, ::fast)
println(result) // → "This function is slow 🐌"
}
Run it here: https://pl.kotl.in/TmKW2jHc2
You can add any flow operator you like to change the rules of the race. Try using filter()
to disqualify some tasks, take(n).toList()
to pick more than one winner, or even last()
to turn the race on its head and have the slowest task win!
Flows and channels aren’t the only way to race coroutines. You can do something similar with select()
, or with the Arrow’s library’s raceN()
function. But the channelFlow
technique gives you the full power of Flow
operators, making it easy to customize the race and set your own rules.
Find more coroutine tips in my upcoming book, Kotlin Coroutine Confidence. Grab your early access beta copy today to be part of the book’s development and get a head start on untangling your async!