RustのAsync/Await入門ガイド
Rust の非同期プログラミングにおける async/.await
async/.await
は Rust に組み込まれた言語機能であり、同期コードのように記述しながら非同期処理を行うことができます。
それでは、async/.await
の使い方を例を通じて学びましょう。始める前に、futures
クレートを追加する必要があります。Cargo.toml
ファイルを編集し、以下の内容を追加してください。
[dependencies]
futures = "0.3"
async を使って非同期 Future を作成する
簡単に言えば、async
キーワードは次のような Future を作成するために使用できます。
- 関数を定義する:
async fn
- ブロックを定義する:
async {}
例えば、async
関数:
async fn hello_world() {
...
}
async
キーワードを付けることで、この関数は Future
trait を実装するオブジェクトを返すようになります。つまり、次のように書き換えられます。
fn hello_world() -> impl Future<Output = ()> {
async { ... }
}
注意:
async
ブロックは匿名のFuture
trait を実装し、Generator
をラップします。Generator
は状態機械(state machine)であり、.await
を使用することで、非同期コードがPoll::Pending
を返した際にyield
を呼び出し、実行権を手放します。一度実行が再開されると、resume
によって残りの処理が進行し、最終的にすべてのコードが完了するとPoll::Ready
を返し、Future の実行が終了します。
async
でマークされたコードブロックは、Future
を実装した状態機械に変換されます。同期処理が現在のスレッドをブロックするのとは異なり、Future
は実行中にブロッキング状態になった場合、スレッドの制御権を他の Future
に譲ります。
Future
を実行するには、実行器(executor)が必要です。例えば block_on
はスレッドをブロックして Future
の実行を完了させることができます。
// block_on は指定した Future が完了するまで現在のスレッドをブロックする。
// これは単純な方法だが、実際のランタイム実行器(executor)はより複雑な振る舞いを提供し、
// 例えば join を使用すると複数の Future を同じスレッド上でスケジューリングできる。
use futures::executor::block_on;
async fn hello_world() {
println!("hello, world!");
}
fn main() {
let future = hello_world(); // Future を返すだけで、まだ実行はされない
block_on(future); // Future を実行し、完了するまでブロックする。"hello, world!" が出力される
}
await を使って他の Future の完了を待つ
上記の main
関数では、block_on
を使って Future
の完了を待つため、一見同期コードのように見えます。しかし、async fn
の中で他の async fn
を呼び出し、その完了を待ってから次の処理を実行したい場合はどうすればよいでしょうか?
例えば、次のコードを見てみましょう。
use futures::executor::block_on;
async fn hello_world() {
// 非同期関数の中で別の非同期関数を呼び出す。問題はないだろうか?
hello_cat();
println!("hello, world!");
}
async fn hello_cat() {
println!("hello, kitty!");
}
fn main() {
let future = hello_world();
block_on(future);
}
このコードでは、hello_world
関数内で hello_cat
を呼び出し、その後に "hello, world!"
を出力しています。実行結果を見てみましょう。
warning: unused implementer of `futures::Future` that must be used
--> src/main.rs:6:5
|
6 | hello_cat();
| ^^^^^^^^^^^^
= note: futures do nothing unless you `.await` or poll them
...
hello, world!
予想通り、main
関数内の Future
は block_on
を通じて実行されましたが、hello_cat
の Future
は実行されていません。しかし、コンパイラは親切に次の警告を出しています:
"futures do nothing unless you
.await
or poll them"
つまり、.await
を使うか、手動で Future
をポーリング(poll)する必要があります。後者は少し複雑なので、まず .await
を使ってみましょう。
use futures::executor::block_on;
async fn hello_world() {
hello_cat().await;
println!("hello, world!");
}
async fn hello_cat() {
println!("hello, kitty!");
}
fn main() {
let future = hello_world();
block_on(future);
}
hello_cat()
に .await
を付けた結果、出力が以下のように変わります。
hello, kitty!
hello, world!
出力の順番がコードの記述順と一致しています。このように .await
を使うことで、非同期処理を自然な順序で記述することができ、コールバック地獄(callback hell)を回避できます。
実際には、.await
自体が実行器のように動作し、内部的には Future
の状態をポーリング(poll)しています。擬似コードで表すと次のようになります。
loop {
match some_future.poll() {
Pending => yield,
Ready(x) => break
}
}
要するに、async fn
の中で .await
を使うことで、他の非同期呼び出しが完了するのを待つことができます。ただし、block_on
とは異なり、.await
はスレッドをブロックせず、Future A
の完了を待つ間に Future B
を実行することができます。この仕組みにより並行処理が実現されます。
例:歌って踊る
await
を使わない場合、以下のようなコードが考えられます。
use futures::executor::block_on;
struct Song {
author: String,
name: String,
}
async fn learn_song() -> Song {
Song {
author: "Rick Astley".to_string(),
name: String::from("Never Gonna Give You Up"),
}
}
async fn sing_song(song: Song) {
println!(
"{} の {} を歌います ~ {}",
song.author, song.name, "Never gonna let you down"
);
}
async fn dance() {
println!("音楽に合わせて踊り始めました!");
}
fn main() {
let song = block_on(learn_song()); // 1回目のブロック
block_on(sing_song(song)); // 2回目のブロック
block_on(dance()); // 3回目のブロック
}
このコードは正しく動作しますが、各タスクを順番にブロックしながら実行しているため、並列実行ができません。しかし、歌いながら踊ることは可能なはずです。
次のコードでは、.await
を活用して並行処理を行います。
use futures::executor::block_on;
struct Song {
author: String,
name: String,
}
async fn learn_song() -> Song {
Song {
author: "Rick Astley".to_string(),
name: String::from("Never Gonna Give You Up"),
}
}
async fn sing_song(song: Song) {
println!(
"{} の {} を歌います ~ {}",
song.author, song.name, "Never gonna let you down"
);
}
async fn dance() {
println!("音楽に合わせて踊り始めました!");
}
async fn learn_and_sing() {
// .await を使用して曲を覚えるのを待つが、スレッドはブロックされない
let song = learn_song().await;
// 歌うには曲を覚えてからでないといけないので、sing_song は learn_song の完了を待つ
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing(); // 曲を覚えて歌う Future
let f2 = dance(); // 踊る Future
// join! マクロを使い、複数の Future を並列で処理する
// learn_and_sing Future がブロックされたら、dance Future が実行される
// dance もブロックされたら、learn_and_sing に制御が戻る
// 両方ともブロックされたら、async_main はブロック状態になり、main の block_on に制御が渡る
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
このコードでは、曲を覚えて歌う処理 と 踊る処理 を並行して実行しています。await
を使わずに block_on(learn_song())
としてしまうと、曲を覚える間スレッドが完全にブロックされ、他の処理(踊るなど)ができなくなります。
したがって、.await
は非同期プログラミングにとって非常に重要であり、同じスレッド内で複数のタスクを並列に実行することを可能にします。
Rust の async/.await の仕組み
ここまでで async/.await
の基本的な使い方は理解できたと思います。次に、その背後でどのように動作しているのかを簡単に説明します。
Rust の async/.await
は、状態機械(state machine) を用いてコードの実行フローを管理します。これは Executor
(実行器)と組み合わせて使用され、協調的なタスクスイッチング(coroutine)を実現します。
async
を使うと、Rust コンパイラはその関数を Future
trait を実装する構造体に変換します。開発者は手動で Future
や poll
メソッドを書く必要はなく、コンパイラが自動的に適切な非同期処理の状態機械を作成してくれます。
まとめ
async/.await
は Rust に組み込まれた非同期プログラミングのためのツールであり、見た目は同期コードのように記述しつつ、内部的には非同期で実行できる というメリットがあります。
-
Future
:非同期タスクを表し、将来的に値を取得できる。Rust では怠惰評価(lazy evaluation)されるため、ポーリング(poll)されるまで実行されない。 -
async
:非同期関数や非同期ブロックを作成し、Future
を返す。 -
.await
:Future
の完了を待つ。内部的にはFuture
の状態をポーリングし、Pending
ならyield
し、Ready
なら処理を続行する。 -
executor
:Future
の管理・実行を担当する。block_on
はスレッドをブロックしてFuture
を実行するシンプルな実行器。 -
Rust の
async/.await
はゼロコストである:ヒープメモリの割り当ても不要で、動的ディスパッチなしで実装される。 -
Rust の
async/.await
には実行時(runtime)が必要:標準ライブラリには非同期実行のためのランタイムが含まれていないため、tokio
、async-std
、smol
などのサードパーティ製の実行環境が必要になる。
結論として、async/.await
は Rust の非同期プログラミングモデルであり、並行タスクの作成と実行のための手段です。async
を使って Future
を生成し、.await
で実行とスケジューリングを制御することで、シンプルかつ強力な非同期処理が実現できます。
私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ
Discussion