Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

非同期プログラミング入門

Rustの非同期処理(async/await)の基礎を学びます。

なぜ非同期処理が必要か

同期処理の問題

#![allow(unused)]
fn main() {
// 同期処理:1つずつ順番に実行
fn fetch_data() {
    let data1 = fetch_from_server1();  // 2秒待つ
    let data2 = fetch_from_server2();  // 2秒待つ
    let data3 = fetch_from_server3();  // 2秒待つ
    // 合計: 6秒
}
}

非同期処理の利点

#![allow(unused)]
fn main() {
// 非同期処理:待ち時間を有効活用
async fn fetch_data() {
    let (data1, data2, data3) = tokio::join!(
        fetch_from_server1(),
        fetch_from_server2(),
        fetch_from_server3(),
    );
    // 合計: 約2秒(並行実行)
}
}

async/await の基本

async関数の定義

#![allow(unused)]
fn main() {
// async関数はFutureを返す
async fn hello() -> String {
    "Hello, async world!".to_string()
}

// 上記は以下と同等
fn hello() -> impl Future<Output = String> {
    async {
        "Hello, async world!".to_string()
    }
}
}

awaitで結果を取得

async fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

async fn main_async() {
    let greeting = greet("Rust").await;  // .awaitで結果を取得
    println!("{}", greeting);
}

Tokioランタイム

非同期コードを実行するには「ランタイム」が必要です。

セットアップ

Cargo.toml

[dependencies]
tokio = { version = "1", features = ["full"] }

基本的な使い方

use tokio;

#[tokio::main]
async fn main() {
    println!("非同期処理を開始");

    let result = async_task().await;
    println!("結果: {}", result);
}

async fn async_task() -> i32 {
    // 非同期処理
    42
}

#[tokio::main]の意味

// これは...
#[tokio::main]
async fn main() {
    // 非同期コード
}

// こう展開される
fn main() {
    tokio::runtime::Runtime::new()
        .unwrap()
        .block_on(async {
            // 非同期コード
        })
}

非同期の待機

tokio::time::sleep

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

async fn delayed_hello() {
    println!("3秒後に挨拶します...");
    sleep(Duration::from_secs(3)).await;
    println!("Hello!");
}

#[tokio::main]
async fn main() {
    delayed_hello().await;
}

注意: std::thread::sleepとの違い

#![allow(unused)]
fn main() {
use std::thread;
use tokio::time::{sleep, Duration};

// ❌ これはスレッド全体をブロック
async fn bad_sleep() {
    thread::sleep(Duration::from_secs(1));  // 他のタスクも止まる
}

// ✅ これは他のタスクに処理を譲る
async fn good_sleep() {
    sleep(Duration::from_secs(1)).await;  // 他のタスクは動ける
}
}

並行実行

tokio::join! - 複数のFutureを並行実行

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

async fn task1() -> i32 {
    sleep(Duration::from_secs(1)).await;
    println!("Task 1 完了");
    1
}

async fn task2() -> i32 {
    sleep(Duration::from_secs(2)).await;
    println!("Task 2 完了");
    2
}

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

    // 並行実行(合計約2秒)
    let (result1, result2) = tokio::join!(task1(), task2());

    println!("結果: {}, {}", result1, result2);
    println!("経過時間: {:?}", start.elapsed());
}

tokio::select! - 最初に完了したものを処理

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

async fn fast_task() -> &'static str {
    sleep(Duration::from_millis(100)).await;
    "fast"
}

async fn slow_task() -> &'static str {
    sleep(Duration::from_secs(10)).await;
    "slow"
}

#[tokio::main]
async fn main() {
    tokio::select! {
        result = fast_task() => {
            println!("Fast finished: {}", result);
        }
        result = slow_task() => {
            println!("Slow finished: {}", result);
        }
    }
    // "Fast finished: fast" と表示(slowはキャンセル)
}

非同期でHTTPリクエスト

reqwestクレートを使用

Cargo.toml

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

基本的なGETリクエスト

use reqwest;

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

    println!("Response: {}", response);
    Ok(())
}

JSONレスポンスの解析

use reqwest;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ApiResponse {
    origin: String,
    url: String,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let response: ApiResponse = reqwest::get("https://httpbin.org/get")
        .await?
        .json()
        .await?;

    println!("Origin: {}", response.origin);
    println!("URL: {}", response.url);
    Ok(())
}

複数のAPIを並行で呼び出し

use reqwest;

async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?.text().await?;
    Ok(response)
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let urls = vec![
        "https://httpbin.org/get",
        "https://httpbin.org/ip",
        "https://httpbin.org/user-agent",
    ];

    // 並行でフェッチ
    let futures: Vec<_> = urls.iter()
        .map(|url| fetch_url(url))
        .collect();

    let results = futures::future::join_all(futures).await;

    for (url, result) in urls.iter().zip(results) {
        match result {
            Ok(body) => println!("{}: {} bytes", url, body.len()),
            Err(e) => println!("{}: エラー - {}", url, e),
        }
    }

    Ok(())
}

エラーハンドリング

Result と ?演算子

use reqwest;
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("HTTPエラー: {0}")]
    Http(#[from] reqwest::Error),

    #[error("JSONパースエラー: {0}")]
    Json(#[from] serde_json::Error),
}

async fn fetch_data(url: &str) -> Result<String, AppError> {
    let response = reqwest::get(url).await?;
    let text = response.text().await?;
    Ok(text)
}

#[tokio::main]
async fn main() {
    match fetch_data("https://httpbin.org/get").await {
        Ok(data) => println!("成功: {} bytes", data.len()),
        Err(e) => eprintln!("エラー: {}", e),
    }
}

タイムアウト

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

#[tokio::main]
async fn main() {
    let result = timeout(
        Duration::from_secs(5),
        reqwest::get("https://httpbin.org/delay/10")
    ).await;

    match result {
        Ok(Ok(response)) => println!("成功: {}", response.status()),
        Ok(Err(e)) => println!("リクエストエラー: {}", e),
        Err(_) => println!("タイムアウト!"),
    }
}

非同期のベストプラクティス

1. ブロッキング処理を避ける

#![allow(unused)]
fn main() {
// ❌ 悪い例
async fn bad_example() {
    std::thread::sleep(Duration::from_secs(1));  // ブロック
    std::fs::read_to_string("file.txt");         // ブロック
}

// ✅ 良い例
async fn good_example() {
    tokio::time::sleep(Duration::from_secs(1)).await;
    tokio::fs::read_to_string("file.txt").await;
}
}

2. 重い処理は spawn_blocking

#![allow(unused)]
fn main() {
use tokio::task;

async fn compute_heavy() -> i32 {
    // CPU重い処理は別スレッドで
    task::spawn_blocking(|| {
        // 重い計算
        (0..1_000_000).sum()
    }).await.unwrap()
}
}

3. エラーは早めに処理

#![allow(unused)]
fn main() {
async fn fetch_and_process(url: &str) -> Result<String, Error> {
    let response = reqwest::get(url).await?;

    // ステータスチェック
    if !response.status().is_success() {
        return Err(Error::HttpStatus(response.status()));
    }

    let text = response.text().await?;
    Ok(text)
}
}

まとめ

概念説明
async非同期関数を定義
.awaitFutureの完了を待つ
tokio非同期ランタイム
join!複数を並行実行
select!最初の完了を処理
注意点対策
ブロッキングtokio::time::sleep等を使用
CPU重い処理spawn_blockingを使用
エラー処理?演算子とResultを活用

確認テスト

Q1. async関数が返すものは?

Q2. tokio::join!(task1(), task2())の動作は?

Q3. 以下のコードの実行時間は約何秒?
tokio::join!(task_a(), task_b())(task_aは2秒sleep、task_bは3秒sleep)

Q4. 非同期関数内でstd::thread::sleepを使うとどうなる?

Q5. tokio::select!の動作として正しいのは?


Phase 4 完了!

おめでとうございます!Phase 4を完了しました。

学んだこと:

  • Cargoの高度な機能(ワークスペース、features)
  • クレートエコシステムの活用
  • テストの書き方(単体テスト、統合テスト)
  • ドキュメンテーション
  • 非同期プログラミング(async/await、tokio)

次のPhase: Phase 5: Webアプリ開発