Rustでの非同期プログラミング:asyncとawait

現代のアプリケーションでは、非同期処理が重要な役割を果たします。

Rustは、asyncawaitというキーワードを使って効率的な非同期プログラミングをサポートしています。

非同期処理は、ブロッキングを避け、リソースを効率的に使用するために重要です。

今回の記事では、Rustにおける非同期プログラミングの基本を解説し、asyncawaitを使ったプログラミング方法を学びます。


非同期処理の基本

非同期処理では、タスクがブロックされることなく他のタスクが進行できるため、効率的なリソースの利用が可能です。

Rustは、シングルスレッドでも高いパフォーマンスを実現する非同期ランタイムを提供し、並行処理を効率的に行うことができます。

asyncawaitの基本

Rustで非同期関数を作成するには、関数の前に async キーワードを付けます。

また、非同期関数を呼び出す際には await キーワードを使って処理を待ちます。

use std::time::Duration;
use tokio::time;

async fn say_hello() {
    println!("Hello, world!");
    time::sleep(Duration::from_secs(2)).await; // 2秒間待つ
    println!("Hello, again!");
}

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

ポイント:

  • async で非同期関数を定義し、非同期に実行できるようにします。
  • await キーワードを使って、非同期処理の完了を待ちます。

非同期ランタイム

非同期処理にはランタイムが必要です。

Rustでは、tokioasync-std などの非同期ランタイムを利用して非同期タスクをスケジュールします。

上記の例では、tokio クレートを使用して非同期タスクを実行しています。


非同期関数と並行処理

非同期関数は、複数のタスクを同時に実行することができ、ブロッキングを避けて効率的な処理を実現します。

複数の非同期タスクを並行して実行するためには、tokio::spawn などを使用します。

非同期タスクの並行処理

次の例では、複数の非同期タスクを並行して実行し、それぞれの完了を待っています。

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

async fn task_one() {
    println!("タスク1開始");
    sleep(Duration::from_secs(2)).await;
    println!("タスク1完了");
}

async fn task_two() {
    println!("タスク2開始");
    sleep(Duration::from_secs(1)).await;
    println!("タスク2完了");
}

#[tokio::main]
async fn main() {
    let t1 = tokio::spawn(task_one());
    let t2 = tokio::spawn(task_two());

    // すべてのタスクが完了するまで待つ
    let _ = tokio::join!(t1, t2);
    println!("すべてのタスクが終了しました");
}

ポイント:

  • tokio::spawn を使って非同期タスクを並行して実行しています。
  • tokio::join! で複数のタスクの完了を待ちます。

非同期処理のエラーハンドリング

非同期関数でも、通常の関数と同様にエラーハンドリングを行うことが重要です。

Result 型を返す非同期関数を定義し、エラーハンドリングを行います。

非同期関数でのエラーハンドリング

use std::error::Error;
use tokio::time::{sleep, Duration};

async fn do_work() -> Result<(), Box<dyn Error>> {
    println!("作業開始");
    sleep(Duration::from_secs(2)).await;
    println!("作業完了");
    Ok(())
}

#[tokio::main]
async fn main() {
    if let Err(e) = do_work().await {
        println!("エラーが発生しました: {}", e);
    }
}

ポイント:

  • 非同期関数でも Result 型を返し、エラーを適切にハンドリングできます。
  • Box<dyn Error> はエラートレイトを動的に使用し、さまざまなエラーを処理します。

非同期処理のキャンセルとタイムアウト

長時間かかる非同期タスクの途中でキャンセルやタイムアウトを実装することができます。

Rustでは、tokio::select! を使ってタスクのキャンセルやタイムアウトを実現できます。

タスクのタイムアウト

次の例では、タスクにタイムアウトを設定し、指定した時間内にタスクが完了しない場合にキャンセルします。

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

async fn slow_task() {
    println!("遅いタスク開始");
    sleep(Duration::from_secs(5)).await;
    println!("遅いタスク完了");
}

#[tokio::main]
async fn main() {
    let result = timeout(Duration::from_secs(3), slow_task()).await;

    match result {
        Ok(_) => println!("タスク完了"),
        Err(_) => println!("タスクがタイムアウトしました"),
    }
}

ポイント:

  • timeout を使うことで、指定時間内にタスクが完了しない場合、タイムアウトエラーを返します。
  • Err(_) でタイムアウトが発生したことを処理しています。

非同期処理の実践例

次に、Webリクエストを非同期で実行する実践的な例を見てみましょう。

この例では、HTTPリクエストを複数のAPIに対して並行して行います。

use reqwest;
use tokio;

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

#[tokio::main]
async fn main() {
    let url1 = "https://www.example.com";
    let url2 = "https://www.rust-lang.org";

    let fetch1 = fetch_url(url1);
    let fetch2 = fetch_url(url2);

    let (res1, res2) = tokio::join!(fetch1, fetch2);

    match res1 {
        Ok(body) => println!("URL1の内容: {}", body),
        Err(e) => println!("URL1の取得に失敗: {}", e),
    }

    match res2 {
        Ok(body) => println!("URL2の内容: {}", body),
        Err(e) => println!("URL2の取得に失敗: {}", e),
    }
}

ポイント:

  • reqwest クレートを使って、HTTPリクエストを非同期で行っています。
  • tokio::join! を使って、複数のリクエストを並行して実行し、その結果を処理しています。

練習問題

練習問題1: 非同期処理でのWebリクエスト

次のコードを完成させ、3つのURLに非同期でリクエストを送り、それぞれの結果を表示してください。

use reqwest;
use tokio;

async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
    // ここでリクエストを実行し、結果を返す
}

#[tokio::main]
async fn main() {
    let url1 = "https://www.example.com";
    let url2 = "https://www.rust-lang.org";
    let url3 = "https://www.github.com";

    // 非同期リクエストを実行し、結果を表示する
}

まとめ

今回の記事では、Rustにおける非同期プログラミングの基本を学びました。

asyncawait を使って効率的な非同期処理を実現し、複数のタスクを並行して実行する方法を理解しました。

非同期ランタイムである tokioasync-std を活用することで、シングルスレッドでも高いパフォーマンスを発揮する非同期処理を行うことができます。

次回予告のデザイン

次回予告

次回は「Rustのモジュールシステムとパッケージ管理」をテーマに、Rustのプロジェクトを効率的に構成し、モジュールとパッケージの管理方法を学びます。