Understanding `tokio::select!` from docs example

This example in the docs is confusing to me, because I would expect the one shot future rx to just get dropped if the "accept" loop every had a chance to start. Why is this not the case? Does select! try more than one time? I have been putting thinking that I needed to put select! into a loop in order to give the other "dropped/ cancelled" futures a chance to "compete" on the next cycle. But perhaps that looping / iterating is done internally somehow. Or maybe I am just misunderstanding this in some other way? Thanks!

################Tokio Docs on Select Below###################

Here, we select on a oneshot and accepting sockets from a TcpListener.

use tokio::net::TcpListener;
use tokio::sync::oneshot;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    let (tx, rx) = oneshot::channel();

    tokio::spawn(async move {
        tx.send(()).unwrap();
    });

    let mut listener = TcpListener::bind("localhost:3465").await?;

    tokio::select! {
        _ = async {
            loop {
                let (socket, _) = listener.accept().await?;
                tokio::spawn(async move { process(socket) });
            }

            // Help the rust type inferencer out
            Ok::<_, io::Error>(())
        } => {}
        _ = rx => {
            println!("terminating accept loop");
        }
    }

    Ok(())
}

The accept loop runs until an error is encountered or rx receives a value. The _ pattern indicates that we have no interest in the return value of the async computation.

Perhaps it is more clear if I write this:

tokio::select! {
    _ = future1 => {}
    _ = future2 => {
        println!("terminating accept loop");
    }
}

In this code snippet, no matter what the two futures are, the select! will poll both futures until one of them completes. When one completes, the other is cancelled. And indeed, it will poll the two futures more than once — it keeps polling both until one completes.

This is exactly what happened in your snippet. It's just that future1 is a rather large expression involving an async block.

It's important to make a distinction between the futures being selected over, and code in the branch:

tokio::select! {
    _ = future1 => {
        do_a_bunch_of_stuff().await;
    }
    _ = future2 => {
        println!("terminating accept loop");
    }
}

The call to do_a_bunch_of_stuff cannot be cancelled by future2, because once the do_a_bunch_of_stuff call starts, that's because future1 has completed, and therefore future2 has already been cancelled when do_a_bunch_of_stuff starts running.

4 Likes

Boom! That is the response I needed to read, especially the last part. I was thinking the "accept" loop was in the handler, not the candidate. I now see that it is the candidate and the handler is empty!

Thanks Alice, you rock!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.