課題17: 非同期プログラミング (Async Programming)

マンダトリー要件

問題1: 非同期HTTPクライアント(30点)

reqwestを使って非同期HTTPクライアントを実装しなさい。

# Cargo.toml に追加
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

use reqwest;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    login: String,
    id: u64,
    node_id: String,
}

// 指定されたURLからJSONを取得
async fn fetch_json<T: for<'de> Deserialize<'de>>(url: &str) -> Result<T, reqwest::Error> {
    // TODO: 実装 (10点)
}

// 複数のURLを並列に取得
async fn fetch_multiple(urls: Vec<&str>) -> Vec<Result<String, reqwest::Error>> {
    // TODO: 実装 (10点)
}

// タイムアウト付きリクエスト
async fn fetch_with_timeout(
    url: &str,
    timeout_secs: u64,
) -> Result<String, Box<dyn std::error::Error>> {
    // TODO: 実装 (10点)
}

#[tokio::main]
async fn main() {
    // テスト1: JSONのフェッチ
    let user: User = fetch_json("https://api.github.com/users/rust-lang")
        .await
        .unwrap();
    println!("User: {:?}", user);

    // テスト2: 並列フェッチ
    let urls = vec![
        "https://example.com",
        "https://rust-lang.org",
    ];
    let results = fetch_multiple(urls).await;
    println!("Fetched {} URLs", results.len());

    // テスト3: タイムアウト
    match fetch_with_timeout("https://httpbin.org/delay/5", 2).await {
        Ok(_) => println!("Success"),
        Err(e) => println!("Timeout or error: {}", e),
    }

    println!("Problem 1 passed!");
}

評価基準:

  • fetch_jsonの実装: 10点
  • fetch_multipleの実装: 10点
  • fetch_with_timeoutの実装: 10点

---

問題2: 非同期タスクスケジューラー(30点)

複数の非同期タスクを管理するスケジューラーを実装しなさい。

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

struct Task {
    id: usize,
    duration_ms: u64,
}

async fn execute_task(task: Task) -> usize {
    // TODO: 実装 (10点)
    // タスクをシミュレート(sleepで待機)
    // タスクIDを返す
}

async fn run_tasks_sequentially(tasks: Vec<Task>) -> Vec<usize> {
    // TODO: 実装 (10点)
    // タスクを順番に実行
}

async fn run_tasks_concurrently(tasks: Vec<Task>) -> Vec<usize> {
    // TODO: 実装 (10点)
    // タスクを並行に実行
}

#[tokio::main]
async fn main() {
    let tasks = vec![
        Task { id: 1, duration_ms: 100 },
        Task { id: 2, duration_ms: 200 },
        Task { id: 3, duration_ms: 150 },
    ];

    // 順次実行
    let start = std::time::Instant::now();
    let results = run_tasks_sequentially(tasks.clone()).await;
    let elapsed = start.elapsed();
    println!("Sequential: {:?} in {:?}", results, elapsed);

    // 並行実行
    let start = std::time::Instant::now();
    let results = run_tasks_concurrently(tasks).await;
    let elapsed = start.elapsed();
    println!("Concurrent: {:?} in {:?}", results, elapsed);

    println!("Problem 2 passed!");
}

評価基準:

  • execute_taskの実装: 10点
  • run_tasks_sequentiallyの実装: 10点
  • run_tasks_concurrentlyの実装: 10点

---

問題3: 非同期チャネル(20点)

tokioのチャネルを使ってプロデューサー・コンシューマーパターンを実装しなさい。

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

async fn producer(tx: mpsc::Sender<i32>, id: usize, count: usize) {
    // TODO: 実装 (10点)
    // count個のメッセージを送信
    // 各メッセージは100msの間隔
}

async fn consumer(mut rx: mpsc::Receiver<i32>) -> Vec<i32> {
    // TODO: 実装 (10点)
    // すべてのメッセージを受信して返す
}

#[tokio::main]
async fn main() {
    let (tx, rx) = mpsc::channel(100);

    // 複数のプロデューサーを起動
    let mut producers = vec![];
    for i in 0..3 {
        let tx = tx.clone();
        producers.push(tokio::spawn(async move {
            producer(tx, i, 5).await;
        }));
    }
    drop(tx);  // オリジナルのsenderをドロップ

    // コンシューマー
    let consumer_handle = tokio::spawn(async move {
        consumer(rx).await
    });

    // すべてのプロデューサーが完了するのを待つ
    for handle in producers {
        handle.await.unwrap();
    }

    let messages = consumer_handle.await.unwrap();
    println!("Received {} messages", messages.len());
    assert_eq!(messages.len(), 15);

    println!("Problem 3 passed!");
}

評価基準:

  • producerの実装: 10点
  • consumerの実装: 10点

---

ボーナス課題

ボーナス1: レートリミッター(10点)

非同期版のレートリミッターを実装しなさい。

use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{Duration, Instant};

struct AsyncRateLimiter {
    max_requests: usize,
    window: Duration,
    requests: Arc<Mutex<Vec<Instant>>>,
}

impl AsyncRateLimiter {
    fn new(max_requests: usize, window: Duration) -> Self {
        // TODO: 実装
    }

    async fn allow(&self) -> bool {
        // TODO: 実装
        // 時間窓内のリクエスト数をチェック
    }

    fn clone_limiter(&self) -> Self {
        // TODO: 実装
    }
}

#[tokio::main]
async fn main() {
    let limiter = AsyncRateLimiter::new(5, Duration::from_secs(1));
    let mut tasks = vec![];

    for i in 0..10 {
        let limiter = limiter.clone_limiter();
        tasks.push(tokio::spawn(async move {
            if limiter.allow().await {
                println!("Request {} allowed", i);
            } else {
                println!("Request {} rate limited", i);
            }
        }));
    }

    for task in tasks {
        task.await.unwrap();
    }

    println!("Bonus 1 passed!");
}

評価基準:

  • 基本的な実装: 5点
  • 非同期ロックの正しい使用: 5点

---

ボーナス2: Webスクレイパー(10点)

複数のURLをクロールして情報を抽出するスクレイパーを実装しなさい。

use reqwest;
use tokio;

struct Page {
    url: String,
    title: String,
    content_length: usize,
}

async fn scrape_page(url: &str) -> Result<Page, Box<dyn std::error::Error>> {
    // TODO: 実装
    // URLからHTMLを取得
    // タイトルとコンテンツサイズを抽出
}

async fn scrape_multiple(urls: Vec<&str>) -> Vec<Result<Page, Box<dyn std::error::Error>>> {
    // TODO: 実装
    // 複数のURLを並列にスクレイプ
}

#[tokio::main]
async fn main() {
    let urls = vec![
        "https://example.com",
        "https://rust-lang.org",
    ];

    let results = scrape_multiple(urls).await;

    for result in results {
        match result {
            Ok(page) => {
                println!("URL: {}", page.url);
                println!("Title: {}", page.title);
                println!("Size: {} bytes", page.content_length);
            }
            Err(e) => println!("Error: {}", e),
        }
    }

    println!("Bonus 2 passed!");
}

評価基準:

  • scrape_pageの実装: 5点
  • scrape_multipleの実装: 5点

---

ボーナス3: カスタムFutureの実装(10点)

遅延評価を行うカスタムFutureを実装しなさい。

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct DelayedValue<T> {
    value: Option<T>,
    ready: bool,
}

impl<T> DelayedValue<T> {
    fn new(value: T) -> Self {
        // TODO: 実装
    }
}

impl<T: Unpin> Future for DelayedValue<T> {
    type Output = T;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // TODO: 実装
        // 最初の呼び出しではPending、2回目以降でReady
    }
}

#[tokio::main]
async fn main() {
    let future = DelayedValue::new(42);
    let result = future.await;
    println!("Result: {}", result);
    assert_eq!(result, 42);

    println!("Bonus 3 passed!");
}

評価基準:

  • Futureの基本実装: 5点
  • 遅延評価のロジック: 5点
  • ---

    評価基準

    マンダトリー部分(80点)

    項目 配点 評価ポイント
    問題1:HTTPクライアント 30点 reqwestの正しい使用
    問題2:タスクスケジューラー 30点 並行/順次実行の理解
    問題3:非同期チャネル 20点 mpscの使用

    ボーナス部分(20点)

    項目 配点 評価ポイント
    ボーナス1:レートリミッター 10点 非同期ロックの理解
    ボーナス2:Webスクレイパー 10点 実用的なアプリケーション
    ボーナス3:カスタムFuture 10点 Futureの内部理解

    : ボーナスは最大20点まで加算されます。

    ---

    提出方法

    ファイル構成

    rust-foundations-17/
    ├── src/
    │   ├── problem1.rs
    │   ├── problem2.rs
    │   ├── problem3.rs
    │   ├── bonus1.rs       # オプション
    │   ├── bonus2.rs       # オプション
    │   └── bonus3.rs       # オプション
    ├── Cargo.toml
    └── README.md
    

    テスト

    cargo test --release
    cargo run --bin problem1
    

    提出期限

  • マンダトリー:第17章学習後、1週間以内
  • ボーナス:第18章修了時まで
  • ---

    ヒント

    問題1のヒント

    // タイムアウト
    use tokio::time::timeout;
    
    let result = timeout(
        Duration::from_secs(timeout_secs),
        reqwest::get(url)
    ).await??;
    

    問題2のヒント

    // 並行実行
    let futures: Vec<_> = tasks
        .into_iter()
        .map(|task| execute_task(task))
        .collect();
    
    futures::future::join_all(futures).await
    

    問題3のヒント

    // プロデューサー
    for i in 0..count {
        tx.send(i as i32).await.unwrap();
        sleep(Duration::from_millis(100)).await;
    }
    

    ---

    学習の確認

    この課題を通じて、以下を理解できたか確認してください:

  • [ ] async/await構文の使い方
  • [ ] Futureトレイトの仕組み
  • [ ] tokioランタイムの基本
  • [ ] 非同期I/O(ファイル、ネットワーク)
  • [ ] タスクのスポーンと管理
  • [ ] join!/select!マクロの使い方
  • [ ] 非同期チャネル(mpsc)

次の章では、unsafe Rustについて学びます。これがRust Foundationsコースの最終章です!