あっぽログ
← 記事一覧に戻る

Rustの非同期プログラミング入門:Tokioで始めるasync/await

Rustの非同期処理とは?

Webサーバーやファイル操作など、「待ち時間」が発生する処理を効率よくこなすために非同期処理が使われます。Rustでは async / await という構文を使って非同期コードを書けますが、それを動かすにはランタイムが別途必要です。

最も広く使われているランタイムが Tokio です。Node.jsでいうイベントループのような役割を担い、非同期タスクを効率よくスケジューリングしてくれます。


Tokioのセットアップ

まず新しいプロジェクトを作り、Cargo.toml に Tokio を追加します。

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

features = ["full"] を指定すると、タイマー・ファイルI/O・ネットワークなどすべての機能が有効になります。学習段階ではこれで問題ありません。


はじめてのasync/await

通常の関数に async をつけると、その関数はFutureを返す関数になります。Futureとは「将来の結果を表す値」で、.await を使って実際の結果を取り出します。

async fn say_hello() {
    println!("Hello, async world!");
}

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

#[tokio::main] マクロを main 関数につけることで、Tokioランタイムが自動的に起動します。これがないと async fn main は動きません。

非同期な待機をシミュレートする

tokio::time::sleep を使って、非同期な「待ち時間」を体験してみましょう。

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

async fn fetch_data(id: u32) -> String {
    // ネットワーク通信などの待ち時間を模擬
    sleep(Duration::from_secs(1)).await;
    format!("データ#{}", id)
}

#[tokio::main]
async fn main() {
    let result = fetch_data(42).await;
    println!("{}", result); // データ#42
}

sleep の前に .await をつけることで、スレッドをブロックせずに他のタスクへ処理を譲れます。これが同期処理との大きな違いです。


複数タスクを並行実行する

非同期処理の真価は複数の処理を同時に走らせるときに発揮されます。tokio::join! マクロを使うと、複数のFutureをまとめて待機できます。

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

async fn fetch_data(id: u32) -> String {
    sleep(Duration::from_secs(1)).await;
    format!("データ#{}", id)
}

#[tokio::main]
async fn main() {
    let (a, b, c) = tokio::join!(
        fetch_data(1),
        fetch_data(2),
        fetch_data(3),
    );
    println!("{}, {}, {}", a, b, c);
    // データ#1, データ#2, データ#3
}

それぞれ1秒かかる処理が3つありますが、join! で並行実行するため合計約1秒で完了します。順番に実行すれば3秒かかるところを大幅に短縮できます。

spawn でバックグラウンドタスクを起動する

tokio::spawn を使うと、タスクをバックグラウンドで起動してすぐに次の処理へ進めます。

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

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(2)).await;
        println!("バックグラウンド処理完了!");
    });

    println!("メイン処理を続行中...");

    // バックグラウンドタスクの終了を待つ
    handle.await.unwrap();
}

出力結果:

メイン処理を続行中...
バックグラウンド処理完了!

spawn が返す JoinHandle.await することで、タスクの完了を待てます。


join! と spawn の使い分け

tokio::join!tokio::spawn
用途結果を全部まとめて待つ独立したタスクを起動
エラー処理各Futureのエラーをそのまま扱えるJoinHandle 経由で扱う
向いている場面関連する複数の非同期処理長時間動くバックグラウンド処理

よくあるミス:awaitを忘れる

// ❌ awaitを忘れた場合
let result = fetch_data(1); // Futureが返るだけで実行されない

// ✅ 正しい書き方
let result = fetch_data(1).await;

.await を書き忘れても Rustコンパイラが警告を出してくれるので、見落とすことは少ないです。ただし「動いているつもりが実行されていない」という状況になりやすいので注意しましょう。


まとめ

  • async / await で非同期関数を定義・呼び出せる
  • Tokioランタイムが非同期タスクを管理・スケジューリングする
  • tokio::join! で複数のFutureを並行実行できる
  • tokio::spawn でバックグラウンドタスクを起動できる

Tokioはエコシステムも豊富で、非同期HTTPクライアントの reqwest や非同期データベースドライバなど、多くのクレートがTokio上で動きます。まずはこの記事の内容を手元で動かして、非同期処理の感覚をつかんでみてください!

← 記事一覧に戻る