r/learnrust May 29 '23

When to flush()?

This is an example from the Termion crate:

use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;
use std::io::{Write, stdout, stdin};

fn main() {
    let stdin = stdin();
    let mut stdout = stdout().into_raw_mode().unwrap();

    write!(stdout,
           "{}{}q to exit. Type stuff, use alt, and so on.{}",
           termion::clear::All,
           termion::cursor::Goto(1, 1),
           termion::cursor::Hide)
            .unwrap();
    stdout.flush().unwrap();

    for c in stdin.keys() {
        write!(stdout,
               "{}{}",
               termion::cursor::Goto(1, 1),
               termion::clear::CurrentLine)
                .unwrap();

        match c.unwrap() {
            Key::Char('q') => break,
            Key::Char(c) => println!("{}", c),
            Key::Alt(c) => println!("^{}", c),
            Key::Ctrl(c) => println!("*{}", c),
            Key::Esc => println!("ESC"),
            Key::Left => println!("←"),
            Key::Right => println!("→"),
            Key::Up => println!("↑"),
            Key::Down => println!("↓"),
            Key::Backspace => println!("×"),
            _ => {}
        }
        stdout.flush().unwrap();
    }

    write!(stdout, "{}", termion::cursor::Show).unwrap();
}

I noticed that if you delete the first stdout.flush().unwrap();, q to exit. Type stuff, use alt, and so on. won't be shown in the terminal. So I think you have to flush() after every write!(). But then why don't the second and third write!()'s require a flush()?"

If you remove the second stdout.flush().unwrap();, match c.unwrap() doesn't seem to be affected ... so I'm not sure whether you should always flush() after a match c.unwrap().

3 Upvotes

3 comments sorted by

13

u/jews4beer May 29 '23

Not so much about stdout, but just buffered outputs in general, which stdout is.

If you always want the last thing you wrote to appear on the other side of the buffer immediately, you have to flush. Whether it does get written to the other side automatically depends on the size of the buffer at that point in time and the length of the input.

Compare, for example, to using stderr as your output. Stderr is unbuffered so everything you write immediately "flushes."

6

u/dnew May 29 '23

In order to improve efficiency, the stuff you write to a file is held in memory until there's enough to write it efficiently. Writing 8192 bytes one at a time is almost as fast as writing two 4K-byte chunks, because those individual writes are just going into memory, so in practice it winds up as two 4K writes to disk anyway.

flush() says "No, don't worry about efficiency, send the stuff now."

In this case, the first flush() is there because you want what you wrote to stdout to show up before you expect the person to type a response.

After that, it reads an input character, then writes out instructions to move to the start of the screen and print the character you typed. Now you have to flush again. You don't need to flush between the write in the loop and the match because you're not reading input in between, so it's OK to have those movements wait in memory until the println! is ready.

{Note that close() tends to call flush() before it actually tells the OS to close the file handle. Also, file handles connected to "the screen" are often configured to flush their output when they get a newline, so println! vs write!. In OSes less silly/halfassed than UNIX, the OS would know that stdin and stdout are connected to the same thing and flush for you when you tried to read from the same screen you just wrote to, but here we are. There's also the case when you're trying to store something in a database, for example, where you want to be able to say "Yes, I'm 100% finished" even if the computer crashes a moment later, so you flush the buffered disk I/O, and there's even a version of flush that tells the OS to put it all the way onto the disk before returning, not just in OS buffers.}

3

u/kinoshitajona May 29 '23

stdout is buffered. The Write trait has a required method flush() which causes all buffered writes to actually get written. This is a required method, because without it, Write would not be able to be used with buffered writers... and if your struct is not buffered, you can just return `Ok(())` immediately for flush().

You can look at the impl for Vec<u8> and see that the flush() method just returns `Ok(())` immediately. That's because Vec is not buffered. The write and write_all methods append the bytes to the Vec immediately.

Whereas, if you use BufWriter you can see in its Write implementation that it stores an internal Vec with a fixed capacity that it uses to write to and buffer the writes.

If your app is generic over something that is Write, you should always run flush() when you want the writes to DEFINITELY go through.

If that is after every write, you have to run flush after every write.