Rustにおける非同期Webサービス:Future、Tokio、async/awaitの徹底解説
Takashi Yamamoto
Infrastructure Engineer · Leapcell

導入
現代のWeb開発において、応答性とスケーラビリティは最重要です。従来の同期プログラミングモデルは、同時I/O操作の要求に対処するのに苦労することが多く、スレッドのブロックや非効率的なリソース利用につながります。そこで非同期プログラミングが輝きを放ち、アプリケーションが非ブロッキング操作を実行し、パフォーマンスを犠牲にすることなく多くのリクエストを同時に処理できるようにします。安全性、パフォーマンス、並行性に重点を置くRustは、堅牢なWebサービスを構築するための優れた選択肢として急速に注目を集めています。Rustの非同期ポテンシャルを解き放つ鍵は、3つの基本的な概念、すなわちFuture
トレイト、Tokioランタイム、そしてasync/await
構文を理解することにあります。この探求では、これらのコアコンポーネントを掘り下げ、Rustにおける効率的で高性能な非同期Web開発を可能にするために、それらがどのように連携して機能するかを説明します。
コアコンセプト解説
メカニズムを掘り下げる前に、Rustの非同期エコシステムを支える基本的な用語を明確に理解しましょう。
Futureトレイト
RustにおけるFuture
は、将来的に完了する可能性のある非同期計算を表すトレイトです。これは、進捗状況を確認するためにポーリングできる列挙型のようなステートマシンです。ポーリングされると、Future
は2つの状態のいずれかを返すことができます。
Poll::Pending
: Futureはまだ準備ができていないため、タスクは後でもう一度ポーリングする必要があります。Poll::Ready(T)
: Futureは完了し、型T
の値が生成されました。
Future
トレイトのコアメソッドはpoll(&mut self, cx: &mut Context<'_>) -> Poll<Self::Output>
です。Context
はWaker
へのアクセスを提供します。これは、Pending
状態になった後、Futureが再度ポーリング可能になったときに実行者に通知するために不可欠です。重要なのは、Future
は「遅延的」であるということです。明示的に実行者によってポーリングされるまで、何も行いません。
Tokioランタイム
Tokioランタイムは、Rustのための非同期ランタイムであり、非同期コードを実行するために必要なすべてを提供します。「asyncエグゼキュータ」とも呼ばれます。なぜなら、Future
を受け取り、完了するまで繰り返しポーリングすることでそれらを「実行」するからです。Tokioは単なるエグゼキュータ以上のものを提供し、包括的なエコシステムを提供します。
- マルチスレッドスケジューラ: 複数のスレッドに
Future
を効率的にディスパッチします。 - 非同期I/Oプリミティブ: comuns I/O操作(TCP、UDP、ファイルなど)の非ブロッキングバージョン。
- タイマー: 特定の時間または遅延後に操作をスケジュールするためのもの。
- 同期プリミティブ: 非同期対応のミューテックス、セマフォ、チャネル。
Tokioは、スレッド管理、タスクスケジューリング、I/O多重化の複雑な詳細を処理し、開発者がアプリケーションロジックに集中できるようにします。
async/await構文
Rustのasync/await
構文は、非同期コードをより人間が読める方法で記述および合成するための、より人間工学的な方法を提供し、同期コードのように見え、感じられるようにします。
async
キーワードは、関数またはブロックを、匿名Future
を返す非同期関数またはブロックに変換します。async
関数を呼び出すと、その本体を実行せずにFuture
がすぐに返されます。async
関数の本体は、返されたFuture
がポーリングされたときにのみ実行されます。await
キーワードは、async
関数またはブロック内でのみ使用できます。これは、待機しているFuture
が完了するまで、現在のasync
関数の実行を一時停止します。await
が待機している間、現在のスレッドをブロックしません。代わりに、実行者に制御を返し、他のFuture
を実行できるようにします。待機していたFuture
が準備完了になると、async
関数は中断したところから再開します。
原理、実装、および応用
これらの概念がどのように連携して非同期Webサービスを構築するかを説明しましょう。
非同期ワークフローの例
I/O操作をシミュレートするシンプルな非同期関数を考えてみましょう。
async fn fetch_data_from_remote() -> String { println!("Fetching data..."); // 時間のかかるネットワークリクエストをシミュレート tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; println!("Data fetched!"); "Hello from remote server!".to_string() }
このasync fn
はFuture<Output = String>
を返します。fetch_data_from_remote()
が呼び出されると、すぐに実行されるのではなく、Future
が作成されるだけです。関数内のawait
は制御を譲り、tokio::time::sleep
FutureがTokioランタイムによって処理されることを可能にし、スレッドをブロックしません。
このFuture
を実行するには、Tokioが提供するエグゼキュータが必要です。
#[tokio::main] async fn main() { println!("Starting application..."); // async関数を呼び出すとFutureが返される let future_data = fetch_data_from_remote(); // AWAITキーワードはFutureが完了するまでポーリングする。 // 待機中、メインスレッドはブロックされない。 let data = future_data.await; println!("Received: {}", data); println!("Application finished."); }
#[tokio::main]
属性は、Tokioが提供する便利なマクロであり、Tokioランタイムをセットアップしてから、そのランタイム内でasync fn main()
関数を実行します。#[tokio::main]
がない場合、手動でランタイムを作成します。
fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("Starting application..."); let data = fetch_data_from_remote().await; println!("Received: {}", data); println!("Application finished."); }); }
rt.block_on()
は、現在のスレッドで単一のFuture
を完了まで実行し、そのFuture
が終了するまでブロックします。block_on
自体はブロッキングですが、それが実行するFuture
(この場合は、非同期ブロック)は、await
するときに実行者に制御を譲ることができます。
Axumを用いたシンプルな非同期Webサーバの構築
これらの概念が、基本的な非同期WebサーバをAxum、Tokio上に構築されたWebフレームワークを使用して構築するためにどのように変換されるかを見てみましょう。
まず、Cargo.toml
に必要な依存関係を追加します。
[dependencies] tokio = { version = "1", features = ["full"] } axum = "0.7"
次に、シンプルなサーバーを実装します。
use axum::{ routing::get, Router, }; use std::net::SocketAddr; // 非同期ハンドラ関数 async fn hello_world() -> String { println!("Handling /hello request..."); // 非同期処理をシミュレート tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; "Hello, Axum and async Rust!".to_string() } // パスパラメータを持つ別の非同期ハンドラ関数 async fn greet_user(axum::extract::Path(name): axum::extract::Path<String>) -> String { println!("Handling /greet request for: {}", name); tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; format!("Greetings, {}! Welcome to async Rust.", name) } #[tokio::main] async fn main() { // アプリケーションルーターを構築 let app = Router::new() .route("/hello", get(hello_world)) .route("/greet/:name", get(greet_user)); // リッスンするアドレスを定義 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Listening on {}", addr); // Hyper(Axumの基盤となるHTTPライブラリ、Tokioを使用)でサーバーを実行 axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
この例では:
hello_world
およびgreet_user
はasync fn
です。/hello
または/greet/:name
へのHTTPリクエストが来ると、Axumはこれらの関数を呼び出し、これらはすぐにFuture
を返します。- Axum(Hyper、Tokio上に構築)はこれらの
Future
を受け取り、Tokioランタイムにスケジュールします。 hello_world
およびgreet_user
内では、tokio::time::sleep().await
は、スレッドをブロックすることなく、現在のリクエストハンドラFuture
の実行を一時停止します。これにより、サーバーは他の着信リクエストを並行して処理できます。axum::Server::bind().serve().await
行は、メインサーバーFuture
を完了まで実行します。このFuture
は、着信接続を継続的にリッスンし、各リクエストに対して新しいタスク(Future
)を作成し、すべてTokioランタイムによって管理されます。
このセットアップにより、1つのリクエストハンドラが長時間実行される非同期操作(データベースや別のAPIからのデータ取得など)を実行している場合でも、サーバーは他のリクエストに対して応答性を維持できます。
結論
Rustの非同期プログラミングモデルは、Future
トレイトを中心に構築され、Tokioランタイムによって駆動され、async/await
によって人間工学的にされ、最新のWeb開発のための堅牢で効率的な基盤を提供します。Future
が非同期計算をどのように表すか、Tokioがそれらをどのように実行するか、そしてasync/await
がそれらの作成と合成をどのように合理化するかを理解することで、開発者はRustのパフォーマンスと安全性のユニークなブレンドを活用して、非常に並行してスケーラブルなWebサービスを構築できます。この強力な組み合わせは、リソース効率や開発者エクスペリエンスを損なうことなく、複雑なロジックを可能にし、要求の厳しいネットワークアプリケーションのためにRustの可能性を最大限に引き出します。信頼性をもって、高速で信頼性が高く、スケーラブルなサービスを構築するために、次の非同期WebプロジェクトにRustを選択してください。