Hi, I am trying to launch several coroutines that ...
# coroutines
n
Hi, I am trying to launch several coroutines that are each computing a result but as soon as one of the computes the correct result I want to cancel all remaining coroutines. Here is the code I wrote but it doesn’t seem like I am doing it correctly. Can anybody improve the code?
Copy code
suspend fun main() {
    println(shortCircuit(1))
    delay(10000)
}

private suspend fun shortCircuit(correctResult: Int): Int? = coroutineScope {
    val channel = Channel<Int?>()
    val n = 100
    val coroutines: List<Job> = List(n) {
        launch {
            try {
                delay(1000 + (it * 1000).toLong())
            } catch(e: CancellationException) {
                println("cancelled $it")
                throw e
            }
            if (it == correctResult) {
                println("sending $it")
                channel.send(it)
            }
            println("Done $it")
        }
    }
    val otherJob = launch {
        coroutines.forEach { it.join() }
        println("sending null")
        channel.send(null)
    }

    val receive = channel.receive()
    otherJob.cancel()
    coroutines.forEach { it.cancel() }
    receive
}
j
I think you should take a look at "racing coroutines" by @louiscad https://blog.louiscad.com/coroutines-racing-why-and-how
☝️ 1
k
Yup. Definitely use
select
here.
1
n
great. Thanks!
d
n
Thanks everyone, It turns out this is not quite what I want. I am not looking for the fastest coroutine. I want them all to compute a result and whichever one computes the required correct result wins and everyone else gets cancelled. In our particular example we are given a session id and we need to search across all servers to find where that session lives currently.
d
https://github.com/Kotlin/kotlinx.coroutines/issues/2867#issuecomment-998553524 should cover your use case as well. However, your use case is simple enough that it's sufficient to create a
CoroutineScope
, launch all the coroutines in that scope, and then, once one of them finds a result, simply cancel the scope as it's no longer needed.
n
the question is how do you do the “Once one of the finds a result” bit. Because some of the will return with null.. and it is possible that none of them find a result.
My code above does that.. but it seems ugly to me.
d
Copy code
val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)
  repeat(100) {
    scope.launch {
      if (it == 7) {
        println("The correct result is found")
        process(it)
        scope.cancel()
      }
    }
  }
After reading your code more closely, here's the solution I like:
Copy code
val answers = Channel<Int?>()
coroutineScope {
  repeat(100) {
    launch {
      if (it == 7) {
        answers.send(it)
        this@coroutineScope.cancel()
      }
    }
  }
}
// we will only be here once all the children of the coroutine scope are finished
answers.tryReceive()
a
Copy code
suspend fun <R> race(vararg races: suspend () -> R): R {
  return channelFlow {
    for (race in races) {
      launch { send(race()) }
     }
   }.first()
 }
n
This will get the fastest coroutine… but I care about the one that produces a non-null result.
l
If the result is null, you can call
awaitCancellation()
n
neat!
What if they all produce null?
l
Then you get a dead-suspend, until there's cancellation
One possible solution is to have a timeout racer that sends whatever value after a given delay
n
it gets messy again. My code above also solves the same problem in a messy way.