到達点は、非同期処理を動かす仕組みと、待ちを制限する場所を理解することです。前提は第07・15回です。
Futureだけでは進まない
async関数はFutureを作り、実行を進めるにはポーリングするランタイムが必要です。awaitは待ちが必要なときに他の仕事へ進む機会を与えます。ここではTokioを使い、ローカルのTCP通信を一往復させます。
新しいプロジェクトのCargo.tomlを次にします。
[package]
name = "async-practice"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "=1.48.0", features = ["rt-multi-thread", "macros", "net", "io-util", "time"] }
src/main.rsへ保存します。
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::time::{timeout, Duration};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
timeout(Duration::from_secs(3), async {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let address = listener.local_addr()?;
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await?;
let mut request = [0; 4];
socket.read_exact(&mut request).await?;
if &request != b"ping" {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "bad request"));
}
socket.write_all(b"pong").await?;
Ok::<(), std::io::Error>(())
});
let mut client = TcpStream::connect(address).await?;
client.write_all(b"ping").await?;
let mut response = [0; 4];
client.read_exact(&mut response).await?;
println!("{}", std::str::from_utf8(&response)?);
server.await??;
Ok::<(), Box<dyn std::error::Error>>(())
}).await??;
Ok(())
}
cargo runでpongが出ます。TCPはバイトの流れで、一回のwriteが一回のreadに対応する保証はありません。ここでは長さを4バイトと決め、read_exactで必要な分を集めています。これは固定長の実習用プロトコルです。HTTPを扱う実アプリでは対応ライブラリを使い、HTTPの境界を自作で推測しません。
タスクの寿命も設計する
timeoutは囲んだFutureの待ちを制限しますが、別にspawnした仕事を自動で全部取り消すわけではありません。この短いアプリはmain終了時にランタイムも終了します。長く動くサーバーならJoinHandleを保持し、キャンセル、待ち合わせ、接続終了を設計します。
CPUを長く占有する計算や同期I/Oを、非同期タスク内でそのまま実行すると他の仕事を妨げます。spawn_blockingや専用ワーカーを検討します。共有ロックを持ったままawaitしないよう、必要な値を取り出してから待つ設計も重要です。
練習と解答
練習:通信相手が3バイトしか送らず接続を閉じたら、read_exactは成功するでしょうか。
解答:4バイトに足りないので失敗します。途中のデータを完全な応答と誤認しないため、メッセージの長さ、最大サイズ、期限をプロトコルとして決めます。
公式資料
Tokio Tutorialとtimeoutが参照先です。
Rust全20回の目次 | 前の回 | 次の回