⚙️

tokio::net::TcpListenerは正しく設定しないとブロッキング動作する場合がある

2025/02/15に公開

環境

tokio のバージョンは 1.43.0
説明のためコードは簡略化している。

発生した問題

tokio::net::TcpListeneraccept で一定時間接続が無かった場合にキャンセル操作を行いたい。
そこで、tokio::time::sleepselect! を使って、タイムアウト処理を実装してみる。(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