
2026/01/18 2:01
**TTYとバッファリング**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Rust の標準出力は、C と異なり常に 行バッファリング を使用するため、出力がパイプで接続されたりリダイレクトされた場合でも同様です。対照的に C プログラムは端末外ではブロック(フル)バッファリングに切り替わるため、数キロバイト分のデータが蓄積してから表示されることがあり、パイプラインで顕著な遅延を引き起こします。Rust の
print! マクロは LineWriter を通じて書き込みを行い、環境に関係なく各行が即座にフラッシュされます。開発者が明示的に制御したい場合は、io::stdout().flush() を呼び出して出力を強制的にフラッシュできます。
標準ライブラリでは現在
Stdout が FIXME コメント付きで実装されており、標準出力が端末に接続されているかどうか(IsTerminal::is_terminal())に応じて LineWriter と BufWriter のどちらを選択するべきだと示されています。Ripgrep などのツールはすでに TTY 検出を利用して色付けやバッファリングを切り替えており、対話型端末には行バッファリング付き出力を、非TTY環境にはブロックバッファリング付き出力を選択します。
Rust の
Stdout が常に行バッファリングをデフォルトとするため、Rust プログラムはコマンドラインセッションと自動化ワークフローの両方で一貫した予測可能な出力を示します。これにより、パイプやリダイレクト時に C のフルバッファリングが引き起こす遅延や不意な挙動と比べてデバッグが簡素化され、驚きが減少します。本文
標準出力は実際にいつフラッシュされるのか?
開発者なら誰もが「バッファがフラッシュされるまで表示されない」プログラムを見たことがあるだろう。代表的な StackOverflow の回答は バッファをフラッシュしなければならない というものだ。以下ではその理由と、Rust が C とどう違うのかを解説する。
簡単な C の例
#include <stdio.h> #include <unistd.h> int main() { for (int i = 1; i <= 5; i++) { printf("line %d\n", i); sleep(1); } return 0; }
ターミナルで実行すると:
$ gcc -o cprint cprint.c $ ./cprint line 1 ← t=1 に表示 line 2 ← t=2 に表示 …
パイプに出力を渡すと状況が変わる:
$ ./cprint | cat ← 5 秒間何も表示されない … line 1 ← t=5 に表示 …
なぜ?
printf() は バッファ に書き込む。TTY(対話型端末)では libc が 行バッファリング を使用し、\n を見たら自動でフラッシュされる。一方、パイプやファイルなどの非 TTY ではデフォルトで 完全バッファリング(約 4–8 KB)になるため、出力全体がバッファに溜まってからしか表示されない。
stderr は特殊で、通常は未バッファ化または行バッファ化されるので、パイプ経由でもエラーメッセージはすぐに表示される。
Rust – 同じ話だが実装は違う
use std::io::{self, Write}; use std::thread::sleep; use std::time::Duration; fn main() { print!("Hello"); sleep(Duration::from_secs(3)); println!(" World!"); }
実行すると:
$ cargo run ← 3 秒間何も表示されない … Hello World!
パイプでも同様:
$ cargo run | cat ← 3 秒間何も表示されない … Hello World!
Rust の
print! マクロは行バッファリングを使っているため、最初の print! は次に来る println! が出るまでフラッシュされない。明示的にフラッシュしたい場合は:
fn main() { print!("Hello"); io::stdout().flush().unwrap(); sleep(Duration::from_secs(3)); println!(" World!"); }
これで得られる出力は:
$ cargo run Hello ← t=0 Hello World! ← t=3
パイプ経由でも同じ結果になる。
TTY の検知
Rust は
std::io::IsTerminal を介して is_terminal() を提供する:
use std::io::{self, IsTerminal}; fn main() { if io::stdout().is_terminal() { println!("TTY!"); } else { println!("NOT TTY!"); } }
実行結果は以下のようになる。
| 実行 | 結果 |
|---|---|
| TTY! |
| `cargo run | cat` |
| NOT TTY! |
実際のユースケース – ripgrep
let printer = StandardBuilder::new() .color_specs(ColorSpecs::default_with_color()) .build(cli::stdout(if std::io::stdout().is_terminal() { ColorChoice::Auto // 端末ならカラーを有効化 } else { ColorChoice::Never // ANSI コードを除去 }));
ripgrep は is_terminal() を使って、色付き出力かどうかを判断する。内部の書き込み先は TTY の場合行バッファリング、パイプの場合ブロックバッファリングになるため、両者のメリットを活かせる。
Rust の stdout が異なる理由
C とは違い、Rust の標準ライブラリは現在 常に
を行バッファリング にしている。ソースコードでは次のようになっている:stdout
pub struct Stdout { inner: &'static ReentrantLock<RefCell<LineWriter<StdoutRaw>>>, }
コメントには「理想的にはターミナルに接続されていないときは
BufWriter に切り替えるべきだが、まだ優先度が高くない」とある。
TL;DR
| 環境 | C の stdout (libc) | Rust の stdout |
|---|---|---|
| TTY | 行バッファリング | 行バッファリング |
| 非 TTY | 完全バッファリング | 行バッファリング |
- C では非 TTY 出力はバッファが埋まるかプログラムが終了するまで遅延する。
- Rust では
は常に改行でフラッシュされる(手動でstdout
を呼ばない限り)。flush()
を使って対話型とパイプ/リダイレクトされた出力を区別し、カラーやバッファリングの挙動を調整できる。is_terminal()