23
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Rustの async/await、最初に見たとき「なにこれ...」ってなりませんでした?

私はなりました。

async fn fetch_data() -> String { ... }

#[tokio::main]
async fn main() {
    let data = fetch_data().await;
}

「asyncってなに?」「awaitってなに?」「tokio::main ってなに?」

この記事では、私が async/await を理解するまでの7ステップを紹介します。

目次

  1. Step 1: なぜ非同期処理が必要か
  2. Step 2: Future とは
  3. Step 3: async/await の基本
  4. Step 4: ランタイムが必要な理由
  5. Step 5: 並行処理のパターン
  6. Step 6: よくあるハマりポイント
  7. Step 7: 実践的な使い方

Step 1: なぜ非同期処理が必要か

同期処理の問題

fn fetch_user() -> User {
    // 1秒かかるとする
    std::thread::sleep(std::time::Duration::from_secs(1));
    User { name: "Alice".to_string() }
}

fn fetch_posts() -> Vec<Post> {
    // 1秒かかるとする
    std::thread::sleep(std::time::Duration::from_secs(1));
    vec![Post { title: "Hello".to_string() }]
}

fn main() {
    let user = fetch_user();    // 1秒待つ
    let posts = fetch_posts();  // さらに1秒待つ
    // 合計2秒
}

問題: I/O待ちの間、CPUが遊んでいる。もったいない。

非同期なら並行実行できる

async fn fetch_user() -> User { ... }
async fn fetch_posts() -> Vec<Post> { ... }

#[tokio::main]
async fn main() {
    // 同時に開始
    let (user, posts) = tokio::join!(fetch_user(), fetch_posts());
    // 合計1秒(並行実行)
}

Step 2: Future とは

Future トレイト

async fn が返すのは Future です。

// 定義(簡略化)
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),    // 完了
    Pending,     // まだ
}

重要な性質

Future は遅延評価。呼んだだけでは実行されない。

async fn hello() {
    println!("Hello!");
}

fn main() {
    let future = hello();  // ← 何も表示されない!
    // futureを.awaitするか、ランタイムに渡さないと実行されない
}

hello() を呼んでも Future が返るだけ。実行するには .await が必要。

Step 3: async/await の基本

async fn

async fnFuture を返す関数を定義する糖衣構文:

// これと
async fn fetch() -> String {
    "data".to_string()
}

// これは大体同じ
fn fetch() -> impl Future<Output = String> {
    async {
        "data".to_string()
    }
}

.await

Future を実行して結果を待つ:

async fn main() {
    let data = fetch().await;  // fetchが完了するまで待つ
    println!("{}", data);
}

async ブロック

関数じゃなくてもasyncは書ける:

let future = async {
    let a = fetch_a().await;
    let b = fetch_b().await;
    a + b
};

Step 4: ランタイムが必要な理由

Rustの非同期はゼロコスト...だが

Rust自体には非同期ランタイムが含まれていない

// これだけではコンパイルエラー
fn main() {
    let data = fetch().await;  // main()はasyncじゃない
}

ランタイムの役割

  1. Futureのスケジューリング: どのFutureを実行するか決める
  2. I/O多重化: epoll/kqueue などで効率的にI/O待ち
  3. タイマー管理: sleep とか timeout とか

代表的なランタイム

ランタイム 特徴
tokio 最も人気。フル機能。
async-std stdに近いAPI。
smol 軽量。シンプル。

tokio の使い方

[dependencies]
tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let data = fetch().await;
}

// ↑ は以下と同じ
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        let data = fetch().await;
    });
}

Step 5: 並行処理のパターン

パターン1: join!(全部待つ)

use tokio::join;

async fn main() {
    let (a, b, c) = join!(
        fetch_a(),
        fetch_b(),
        fetch_c(),
    );
    // 全部完了するまで待つ
}

パターン2: select!(最初の1つ)

use tokio::select;

async fn main() {
    select! {
        result = fetch_a() => println!("A finished: {:?}", result),
        result = fetch_b() => println!("B finished: {:?}", result),
    }
    // 最初に完了したものだけ処理
}

パターン3: spawn(バックグラウンド実行)

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // バックグラウンドで実行
        heavy_computation().await
    });
    
    // 他の処理
    do_something().await;
    
    // 結果を待つ
    let result = handle.await.unwrap();
}

パターン4: 複数タスクの並行処理

use futures::future::join_all;

async fn fetch_all(urls: Vec<String>) -> Vec<Response> {
    let futures: Vec<_> = urls.iter()
        .map(|url| fetch(url))
        .collect();
    
    join_all(futures).await
}

Step 6: よくあるハマりポイント

ハマり1: async fn は Send でないといけない(場合がある)

async fn bad_example() {
    let rc = std::rc::Rc::new(42);  // RcはSendじゃない
    tokio::spawn(async move {
        println!("{}", rc);  // コンパイルエラー!
    }).await;
}

tokio::spawn に渡す Future は Send が必要。RcSend じゃないのでエラー。

解決策: Arc を使う。

async fn good_example() {
    let arc = std::sync::Arc::new(42);
    tokio::spawn(async move {
        println!("{}", arc);
    }).await;
}

ハマり2: .await の位置

// ❌ 間違い: ロックを持ったまま.await
async fn bad() {
    let mutex = tokio::sync::Mutex::new(0);
    let mut guard = mutex.lock().await;
    *guard += 1;
    some_async_fn().await;  // ロックを持ったまま待機
}  // ここでロック解放

// ⭕ 正しい: .await前にロック解放
async fn good() {
    let mutex = tokio::sync::Mutex::new(0);
    {
        let mut guard = mutex.lock().await;
        *guard += 1;
    }  // ここでロック解放
    some_async_fn().await;
}

ハマり3: blocking 処理

// ❌ 間違い: asyncの中でブロッキング処理
async fn bad() {
    std::thread::sleep(Duration::from_secs(1));  // ランタイムをブロック!
}

// ⭕ 正しい: tokio::time::sleepを使う
async fn good() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

// ⭕ またはspawn_blockingで別スレッドに逃がす
async fn also_good() {
    tokio::task::spawn_blocking(|| {
        heavy_cpu_work()  // CPUバウンドな処理
    }).await.unwrap();
}

ハマり4: async トレイトメソッド

// 現在のRustでは直接書けない
trait MyTrait {
    async fn do_something(&self);  // エラー!
}

解決策: async-trait クレートを使う

use async_trait::async_trait;

#[async_trait]
trait MyTrait {
    async fn do_something(&self);
}

#[async_trait]
impl MyTrait for MyStruct {
    async fn do_something(&self) {
        // ...
    }
}

Step 7: 実践的な使い方

HTTPクライアント(reqwest)

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let body = reqwest::get("https://httpbin.org/get")
        .await?
        .text()
        .await?;
    
    println!("{}", body);
    Ok(())
}

タイムアウト

use tokio::time::{timeout, Duration};

async fn with_timeout() -> Result<String, &'static str> {
    match timeout(Duration::from_secs(5), fetch_data()).await {
        Ok(result) => Ok(result),
        Err(_) => Err("タイムアウト"),
    }
}

リトライ

async fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, Error> {
    let mut attempts = 0;
    
    loop {
        match fetch(url).await {
            Ok(data) => return Ok(data),
            Err(e) if attempts < max_retries => {
                attempts += 1;
                tokio::time::sleep(Duration::from_secs(1 << attempts)).await;
            }
            Err(e) => return Err(e),
        }
    }
}

チャネル

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);
    
    tokio::spawn(async move {
        for i in 0..10 {
            tx.send(i).await.unwrap();
        }
    });
    
    while let Some(value) = rx.recv().await {
        println!("received: {}", value);
    }
}

まとめ

7ステップのおさらい

  1. なぜ非同期か: I/O待ちを効率化
  2. Future: 遅延評価される計算
  3. async/await: Future を楽に書く構文
  4. ランタイム: Future を実行する仕組み
  5. 並行処理: join!, select!, spawn
  6. ハマりポイント: Send, ロック, blocking
  7. 実践: reqwest, timeout, channel

チェックリスト

  • async fn は Future を返すだけで、呼んでも実行されない
  • .await で実際に実行される
  • ランタイム(tokio等)がないと動かない
  • blocking 処理は spawn_blocking で逃がす
  • std::sync::Mutex じゃなくて tokio::sync::Mutex を使う

今すぐできるアクション

  1. tokio を入れて簡単な async/await を書いてみる
  2. join! で並行処理を体験
  3. reqwest で HTTP リクエストを投げてみる

async/await、最初は意味不明だったけど、「Future = 遅延評価」「ランタイムが実行する」がわかったら急に理解が進みました。

みなさんも頑張ってね!

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

23
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?