Read async from stdin using tokio with timeout

It appears that the read is not properly cancelled when the timeout is hit. What am I doing wrong here?

#[tokio::main]
async fn main() {
    let mut buf = String::new();
    match tokio::time::timeout(tokio::time::Duration::from_secs(1), tokio::io::stdin().read_to_string(&mut buf)).await {
        Ok(Ok(_)) => {
            println!("Read line: {}", buf);
        },
        Ok(Err(e)) => {
            println!("Error reading line: {:?}", e);
        },
        Err(_) => {
            println!("Timeout");
        }
    }

    println!("program complete");
}

prints

Timeout
program complete

But never exits. It will exit if I hit enter. I would expect it to exit immediately after printing "program complete".
What is happening here and how can I get the desired behavior?

This is a limitation of Tokio: https://docs.rs/tokio/latest/tokio/io/fn.stdin.html

For technical reasons, stdin is implemented by using an ordinary blocking read on a separate thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang until the user presses enter.

The easy way around is to just exit the process instead of return from main. But keep in mind this won't run destructors for anything that's still alive at that point.

#[tokio::main]
async fn main() {
    let mut buf = String::new();
    match tokio::time::timeout(tokio::time::Duration::from_secs(1), tokio::io::stdin().read_to_string(&mut buf)).await {
        Ok(Ok(_)) => {
            println!("Read line: {}", buf);
        },
        Ok(Err(e)) => {
            println!("Error reading line: {:?}", e);
        },
        Err(_) => {
            println!("Timeout");
        }
    }

    println!("program complete");
    std::process::exit(0);
}

You can also create the runtime manually and shut it down manually, which is good if you want to drop the runtime (eventually) but not end the program.

The proper way to do this is to ask the OS for a non-blocking handle to stdin, but tokio doesn't do this. There's some reasoning here: Using `stdin` prevents `tokio::run` from shutting down. · Issue #589 · tokio-rs/tokio · GitHub

2 Likes

I see. That is quite surprising, thank you for the detailed explanation.

1 Like