tokio::net::TcpListenerは正しく設定しないとブロッキング動作する場合がある
環境
tokio のバージョンは 1.43.0
。
説明のためコードは簡略化している。
発生した問題
tokio::net::TcpListener
の accept
で一定時間接続が無かった場合にキャンセル操作を行いたい。
そこで、tokio::time::sleep
と select!
を使って、タイムアウト処理を実装してみる。(tokio::time::timeout
を使うのが一番良いが、ログ挿入の都合上この方式を取っている)。
fn print_with_time(msg: &str) {
let now = std::time::SystemTime::now();
let since_the_epoch = now.duration_since(std::time::UNIX_EPOCH).unwrap();
let millis = since_the_epoch.as_millis();
let t = (millis % 100000) as f64 / 1000.0;
eprintln!("[{}] {}", t, msg);
}
#[tokio::main]
pub async fn main() {
let listener = std::net::TcpListener::bind("localhost:8999").unwrap();
let tokio_listener = tokio::net::TcpListener::from_std(listener).unwrap();
loop {
print_with_time("[1]");
let ret = tokio::select! {
_ = async {
print_with_time("[b11]");
let _ret = tokio_listener.accept().await;
print_with_time("[b12]");
} => 1,
_ = async {
print_with_time("[b21]");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
print_with_time("[b22]");
} => 2,
};
print_with_time(format!("[2] {}", ret).as_str());
}
}
実行結果は以下の通り。//
から始まる行はコメントで実際のログではありません。
[66.045] [1]
[66.045] [b11]
[66.045] [b21]
[68.047] [b22]
[68.047] [2] 2
[68.047] [1]
[68.047] [b11]
[68.047] [b21]
[70.049] [b22]
[70.049] [2] 2
[70.049] [1]
[70.049] [b11]
[70.049] [b21]
// 1度目のTCP接続
[70.626] [b12]
[70.627] [2] 1
[70.627] [1]
[70.627] [b21]
[70.627] [b11]
// 2度目のTCP接続 タイムアウト[b22]が発生していない。
[80.036] [b12]
[80.036] [2] 1
[80.036] [1]
[80.036] [b11]
1度目のTCP接続を受信するまではタイムアウトは正しく動作する。
2度目以降はタイムアウト(具体的にはtokio::time::sleep
)が正しく動作していない。
sleep が動作していない、ということはどういうことだろう?
原因
以下の行に原因がある。std::net::TcpListener
でサーバを作成し、tokio::net::TcpListener
で変換している。
let listener = std::net::TcpListener::bind("localhost:8999").unwrap();
let tokio_listener = tokio::net::TcpListener::from_std(listener).unwrap();
ドキュメントを見ると、from_std
を使うときは、Non-blocking モードをセットするよう義務付けられている。上のコードでは non_blocking を使っていない。
https://docs.rs/exstd/latest/exstd/tokio/net/struct.TcpListener.html#method.from_std
The caller is responsible for ensuring that the listener is in non-blocking mode. Otherwise all I/O operations on the listener will block the thread, which will cause unexpected behavior. Non-blocking mode can be set using set_nonblocking.
解決策
ノンブロッキングモードにする。
let listener = std::net::TcpListener::bind("localhost:8999").unwrap();
listener.set_nonblocking(true).unwrap();
let tokio_listener = tokio::net::TcpListener::from_std(listener).unwrap();
あるいは、tokio::net::TcpListener::bind
を使う。
let tokio_listener = tokio::net::TcpListener::bind("localhost:8999").await.unwrap();
感想
多少ブロッキングしても良いならselectシステムコールで待機出来るんですけどね…。
Discussion