Skip to Content
Course content

103: select Expression for Coroutines

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was working on a financial dashboard that needed to pull real-time exchange rates. We had two different API providers: one was incredibly fast but had a habit of timing out or returning 500s during peak volatility; the other was a rock-solid legacy system that was consistently slow. My teammate's initial approach was to call the fast one, wait for it to fail, and then call the slow one. The problem? When the fast API hung for 5 seconds before failing, the user just stared at a loading spinner, even though the slow API could have finished in 2 seconds. We needed a way to "race" the two calls and just take whoever crossed the finish line first.

This is exactly where the select expression comes in. In most of your coroutine work, you'll be using await() or receive(), which are blocking (in the suspending sense)—they stop execution until that one specific operation finishes. But select allows you to wait for multiple suspending operations simultaneously and execute a block of code for the first one that becomes available. It's essentially a type-safe way of saying, "I don't care who wins, just give me the first result that arrives."

Racing Deferred Results

When you're dealing with Deferred values (the results of async), you use the onAwait clause inside a select block. I like to think of this as a competition. Instead of waiting for deferredA and then deferredB, select monitors both. The moment one completes, its corresponding block runs, and the other operation is effectively ignored by the select block (though the coroutine is still running in the background unless you explicitly cancel it).

import kotlinx.coroutines.*
import kotlinx.coroutines.selects.*

suspend fun fetchFastApi() = async {
    delay(1000) // Simulate network
    "Fast API Result"
}

suspend fun fetchSlowApi() = async {
    delay(2000) // Simulate network
    "Slow API Result"
}

suspend fun raceApis() = coroutineScope {
    val fast = fetchFastApi()
    val slow = fetchSlowApi()

    val winner = select<String> {
        fast.onAwait { it }
        slow.onAwait { it }
    }
    println("The winner is: $winner")
}

One detail you should notice: select is currently marked as @ExperimentalCoroutinesApi. Don't let that scare you off, but be aware that you'll likely need the annotation on your function. Honestly, it's been in this state for a while, and it's a staple for anyone building complex asynchronous pipelines.

Multiplexing with Channels

While racing Deferred values is common, select really shines when you're managing multiple Channels. Imagine you're building a chat application where you're listening to a socket for messages, but you also need to listen for a "cancel" signal from the UI thread. You can't call channel.receive() on the socket and then cancelChannel.receive() on the signal, because you'd be stuck waiting for a message before you'd ever check if the user clicked cancel.

By using onReceive (or onReceiveCatching for safer handling), you can merge these streams into a single loop. Here is how I usually structure that pattern:

val messages = Channel<String>()
val signals = Channel<Signal>()

while (isActive) {
    val action = select<String> {
        messages.onReceive { msg -> 
            "Processing message: $msg" 
        }
        signals.onReceive { sig -> 
            "Handling signal: $sig" 
        }
    }
    println(action)
}

The beauty of this is that it keeps your logic sequential. You aren't juggling callbacks or managing complex state flags; you're simply reacting to whichever event happens first in time.




📋 Practical Task

Implement a Redundant Data Fetcher

You are building a system that must fetch a configuration file from two different mirrors. To ensure the lowest possible latency, you must launch requests to both mirrors simultaneously and return the result of whichever one responds first. If one fails, the select expression should still wait for the other (or you can handle the exception within the onAwait block).

Requirements:

  • Create two functions, fetchFromMirrorA() and fetchFromMirrorB(), both returning a Deferred<String>.
  • Simulate varying network speeds using delay() with random values between 100ms and 1000ms.
  • Use a select expression to capture the first successful response.
  • Print which mirror provided the result and the content of that result.
  • Ensure the final code is wrapped in a coroutineScope to manage the lifecycle of the async tasks.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.