Zenn
🍼

RustのAsync/Await入門ガイド

2025/03/21に公開

表紙

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 関数内の Futureblock_on を通じて実行されましたが、hello_catFuture は実行されていません。しかし、コンパイラは親切に次の警告を出しています:

"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 を実装する構造体に変換します。開発者は手動で Futurepoll メソッドを書く必要はなく、コンパイラが自動的に適切な非同期処理の状態機械を作成してくれます。

まとめ

async/.await は Rust に組み込まれた非同期プログラミングのためのツールであり、見た目は同期コードのように記述しつつ、内部的には非同期で実行できる というメリットがあります。

  • Future :非同期タスクを表し、将来的に値を取得できる。Rust では怠惰評価(lazy evaluation)されるため、ポーリング(poll)されるまで実行されない。
  • async :非同期関数や非同期ブロックを作成し、Future を返す。
  • .awaitFuture の完了を待つ。内部的には Future の状態をポーリングし、Pending なら yield し、Ready なら処理を続行する。
  • executorFuture の管理・実行を担当する。block_on はスレッドをブロックして Future を実行するシンプルな実行器。
  • Rust の async/.await はゼロコストである:ヒープメモリの割り当ても不要で、動的ディスパッチなしで実装される。
  • Rust の async/.await には実行時(runtime)が必要:標準ライブラリには非同期実行のためのランタイムが含まれていないため、tokioasync-stdsmol などのサードパーティ製の実行環境が必要になる。

結論として、async/.await は Rust の非同期プログラミングモデルであり、並行タスクの作成と実行のための手段です。async を使って Future を生成し、.await で実行とスケジューリングを制御することで、シンプルかつ強力な非同期処理が実現できます。


私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

Discussion

ログインするとコメントできます