Why does Future::select choose the future with a longer sleep period first?

TL;DR: use tokio::time

If there’s one thing to take away from this: never perform blocking or long-running operations inside of asynchronous operations.

If you want a timeout, use something from tokio::time, such as delay_for or timeout:

use futures::future::{self, Either}; // 0.3.1
use std::time::Duration;
use tokio::time; // 0.2.9

#[tokio::main]
async fn main() {
    let time_out1 = time::delay_for(Duration::from_secs(5));
    let time_out2 = time::delay_for(Duration::from_secs(1));

    match future::select(time_out1, time_out2).await {
        Either::Left(_) => println!("Timer 1 finished"),
        Either::Right(_) => println!("Timer 2 finished"),
    }
}

What’s the problem?

To understand why you get the behavior you do, you have to understand the implementation of futures at a high level.

When you call run, there’s a loop that calls poll on the passed-in future. It loops until the future returns success or failure, otherwise the future isn’t done yet.

Your implementation of poll “locks up” this loop for 5 seconds because nothing can break the call to sleep. By the time the sleep is done, the future is ready, thus that future is selected.

The implementation of an async timeout conceptually works by checking the clock every time it’s polled, saying if enough time has passed or not.

The big difference is that when a future returns that it’s not ready, another future can be checked. This is what select does!

A dramatic re-enactment:

sleep-based timer

core: Hey select, are you ready to go?

select: Hey future1, are you ready to go?

future1: Hold on a seconnnnnnnn [… 5 seconds pass …] nnnnd. Yes!

simplistic async-based timer

core: Hey select, are you ready to go?

select: Hey future1, are you ready to go?

future1: Checks watch No.

select: Hey future2, are you ready to go?

future2: Checks watch No.

core: Hey select, are you ready to go?

[… polling continues …]

[… 1 second passes …]

core: Hey select, are you ready to go?

select: Hey future1, are you ready to go?

future1: Checks watch No.

select: Hey future2, are you ready to go?

future2: Checks watch Yes!

This simple implementation polls the futures over and over until they are all complete. This is not the most efficient, and not what most executors do.

See How do I execute an async/await function without using any external dependencies? for an implementation of this kind of executor.

smart async-based timer

core: Hey select, are you ready to go?

select: Hey future1, are you ready to go?

future1: Checks watch No, but I’ll call you when something changes.

select: Hey future2, are you ready to go?

future2: Checks watch No, but I’ll call you when something changes.

[… core stops polling …]

[… 1 second passes …]

future2: Hey core, something changed.

core: Hey select, are you ready to go?

select: Hey future1, are you ready to go?

future1: Checks watch No.

select: Hey future2, are you ready to go?

future2: Checks watch Yes!

This more efficient implementation hands a waker to each future when it is polled. When a future is not ready, it saves that waker for later. When something changes, the waker notifies the core of the executor that now would be a good time to re-check the futures. This allows the executor to not perform what is effectively a busy-wait.

The generic solution

When you have have an operation that is blocking or long-running, then the appropriate thing to do is to move that work out of the async loop. See What is the best approach to encapsulate blocking I/O in future-rs? for details and examples.

Leave a Comment