17. async/await 入門

🎯 この章のゴール

  • async / await と Future の関係を理解する
  • ランタイム(tokio)を使って非同期コードを動かす
  • スレッドとの使い分けを知る

💡 ひとことで言うと

async は ラーメン屋の呼び出しベル。注文(リクエスト)したら席で待ち続けるのではなく、ベルが鳴るまで他のことをしていられます。

同期 vs 非同期

同期:  [A を待つ......][B を待つ......][C を待つ......]
非同期:[A を待つ......]
       [B を待つ......]   ← 待ち時間を重ねられる
       [C を待つ......]

Web サーバーのように「待ち」が多い処理では、何千ものリクエストを少ないスレッドで捌けます。

async と Future

async fn greet(name: &str) -> String {
    format!("こんにちは、{name}")
}
// ↑ は概念的には次と同じ:
// fn greet(name: &str) -> impl Future<Output = String>
Playground で開く ↗
  • async fn を呼ぶと、すぐには実行されず Future(あとで値になるもの) が返る
  • .await で Future の完了を待ち、結果を取り出す
  • .await は async の中でしか使えない

ランタイムで動かす

標準ライブラリには Future を実行する仕組み(ランタイム)が含まれていないので、tokio を使います。

cargo add tokio --features full
use std::time::Duration;
use tokio::time::sleep;

async fn cook(dish: &str, secs: u64) -> String {
    sleep(Duration::from_secs(secs)).await; // 待っている間、他の処理に譲る
    format!("{dish} できあがり!")
}

#[tokio::main]
async fn main() {
    // 順番に待つ: 合計 3 秒
    let a = cook("ラーメン", 2).await;
    let b = cook("餃子", 1).await;
    println!("{a} {b}");

    // 並行に待つ: 合計 2 秒
    let (a, b) = tokio::join!(cook("ラーメン", 2), cook("餃子", 1));
    println!("{a} {b}");

    // タスクとして起動(スレッドより遥かに軽い)
    let handle = tokio::spawn(async { cook("チャーハン", 1).await });
    println!("{}", handle.await.unwrap());
}
Playground で開く ↗

便利な道具

道具役割
tokio::join!(a, b)すべて完了するまで待つ
tokio::select! { .. }最初に完了したものを採用(タイムアウト処理など)
tokio::spawn(fut)独立したタスクとして実行
tokio::sync::mpscasync 版チャネル

スレッドとの使い分け

スレッドasync
得意CPU を使う計算I/O 待ちが多い処理
1 つあたりのコスト重い(数 KB〜MB のスタック)軽い(数百バイト程度)
必要なもの標準ライブラリだけランタイム(tokio など)

async の中で重い計算や std::thread::sleep のような ブロッキング処理 をすると、ランタイム全体が止まってしまいます。重い処理は tokio::task::spawn_blocking に逃がしましょう。

🧪 やってみよう

下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。

3 品の料理を tokio::join! で並行に作って、全体の所要時間を計測してみよう(Playground でも tokio が使えます)。

use std::time::{Duration, Instant};
use tokio::time::sleep;

async fn cook(dish: &str, secs: u64) -> String {
    sleep(Duration::from_secs(secs)).await;
    format!("{dish} できあがり!")
}

#[tokio::main]
async fn main() {
    let start = Instant::now();

    // TODO: tokio::join! で 3 品を並行に作ろう(今は 1 品ずつ順番)
    let a = cook("ラーメン", 2).await;
    let b = cook("餃子", 1).await;
    let c = cook("チャーハン", 1).await;
    println!("{a} {b} {c}");

    println!("所要時間: {:.1?}", start.elapsed()); // 並行なら約 2 秒
}
Playground で開く ↗
💡 解答例を見る
use std::time::{Duration, Instant};
use tokio::time::sleep;

async fn cook(dish: &str, secs: u64) -> String {
    sleep(Duration::from_secs(secs)).await;
    format!("{dish} できあがり!")
}

#[tokio::main]
async fn main() {
    let start = Instant::now();

    let (a, b, c) = tokio::join!(
        cook("ラーメン", 2),
        cook("餃子", 1),
        cook("チャーハン", 1),
    );
    println!("{a} {b} {c}");

    println!("所要時間: {:.1?}", start.elapsed()); // 並行なら約 2 秒
}
Playground で開く ↗

📝 まとめ

  • async fn は Future を返し、.await で完了を待つ
  • Future は怠惰。実行にはランタイム(tokio など)が必要
  • I/O 待ちには async、CPU 計算にはスレッド

✅ 理解度クイズ

Q1
async fn fetch() -> String を呼び出して fetch() と書いただけのとき、何が起きる?
Q2
スレッドと比べた async の主な利点は?
Q3
async fn main() を標準ライブラリだけで書けない理由は?
Q4
2 つの Future を 並行に 実行して両方の完了を待つには?