Rustのasync/await
レストランを思い浮かべてください。並行処理のページでは、スレッドについて学びました。スレッドは、テーブルごとにウェイターを1人ずつ雇うようなものです。それでもお店は回りますが、ウェイターを雇うのはお金がかかりますし、ほとんどの時間は厨房の料理を待って立っているだけです。
async(非同期処理)は、1人のウェイターがたくさんの注文を受けるようなものです。ウェイターはテーブル1の注文を取って厨房に伝え、料理ができるのを待つあいだにテーブル2へ向かいます。誰もぼんやり立っていません。これが Rust の async と .await の考え方です。あるタスクが(ネットワークやファイル、タイマーを)待っているあいだに、別のタスクを動かすのです。
async が力を発揮するのは、プログラムが待ち時間の多い仕事をするときです。Web サーバー、API クライアント、チャットアプリ、ボットなどがそうです。このページでは、Rust でいちばん人気のある非同期ランタイム tokio を使います。
tokio の準備
Section titled “tokio の準備”Rust の標準ライブラリには async と .await というキーワードはありますが、非同期のコードを実際に動かす「エンジン」は入っていません。このエンジンをランタイム(runtime)と呼び、ほとんどのプロジェクトでは tokio が使われています。新しいプロジェクトに追加してみましょう。
cargo new async_democd async_democargo add tokio --features fullすると、Cargo.toml に次の行が追加されます。
[dependencies]tokio = { version = "1", features = ["full"] }full フィーチャーを指定すると、すべての機能(タイマー、タスク、ネットワークなど)が有効になります。学んでいるうちは、これがいちばん手軽です。このページの「▶ 実際に試してみよう」ボタンも使えます。Rust Playground には tokio が最初から入っているからです。
はじめての非同期プログラム
Section titled “はじめての非同期プログラム”use std::time::Duration;
async fn make_coffee() -> String { tokio::time::sleep(Duration::from_millis(20)).await; String::from("コーヒー")}
#[tokio::main]async fn main() { println!("注文しています..."); let drink = make_coffee().await; println!("{}をどうぞ", drink);}実行結果:
注文しています...コーヒーをどうぞここには新しいものが3つあります。
async fnは、途中で一時停止して、あとから続きを実行できる関数を作ります。.awaitは「これが終わるのを待つ。待っているあいだは、ほかのタスクを動かしてよい」という意味です。.awaitはasyncな関数の中でしか使えません。#[tokio::main]は tokio のランタイムを起動し、その上でasync fn mainを実行します。これがないと、mainをasyncにはできません。
tokio::time::sleep は「少し待つ」の非同期版です。止まるのはこのタスクだけで、スレッド全体は止まりません。
Future は遅延評価される
Section titled “Future は遅延評価される”ここが、多くの人が驚くところです。async fn を呼び出しても、その中身は実行されません。代わりに Future(フューチャー)が返ってきます。Future とは「あとで行われる作業」を表す値です。.await するまで、何も起こりません。
async fn say_hello() { println!("Future の中からこんにちは!");}
#[tokio::main]async fn main() { let future = say_hello(); // まだ何も実行されない println!("Future を作ったけれど、まだ始まっていません");
future.await; // ここで実行される println!("完了");}実行結果:
Future を作ったけれど、まだ始まっていませんFuture の中からこんにちは!完了Future は、注文を書いた伝票のようなものだと考えてください。伝票を書いただけでは料理はできません。厨房に渡して(.await して)はじめて料理が作られます。
join! で複数のタスクを同時に動かす
Section titled “join! で複数のタスクを同時に動かす”ここまでは、一度に1つのことを待っていました。本当の力を発揮するのは、いくつものことを同時に待つときです。tokio::join! は複数の Future をまとめて動かし、すべてが終わるまで待ちます。結果はタプルで返ってきます。
use std::time::Duration;use tokio::time::sleep;
async fn fetch_user() -> String { sleep(Duration::from_millis(30)).await; // ネットワーク通信のつもり String::from("アナ")}
async fn fetch_order_count() -> u32 { sleep(Duration::from_millis(20)).await; 3}
#[tokio::main]async fn main() { let (user, orders) = tokio::join!(fetch_user(), fetch_order_count()); println!("{}さんの注文は{}件です", user, orders);}実行結果:
アナさんの注文は3件です1つずつ順番に .await したら、待ち時間の合計はおよそ 30 + 20 = 50ms になります。join! なら2つの待ち時間が同時に進むので、いちばん遅いものと同じ、およそ 30ms で済みます。本物のネットワーク通信では、この差はすぐに大きくなります。
tokio::spawn でタスクを起動する
Section titled “tokio::spawn でタスクを起動する”join! は、Future の数が決まっているときに便利です。仕事のリストがあるときは tokio::spawn を使いましょう。tokio::spawn は Future を独立したタスクとしてランタイムに渡し、すぐに実行を始めて、JoinHandle を返します。あとでそのハンドルを .await すると、結果を受け取れます。
use std::time::Duration;use tokio::time::sleep;
async fn download(id: u64) -> String { sleep(Duration::from_millis(50 - id * 10)).await; format!("ファイル{}のダウンロードが完了しました", id)}
#[tokio::main]async fn main() { let mut handles = Vec::new();
for id in 1..=3 { let handle = tokio::spawn(download(id)); // ここで実行が始まる handles.push(handle); }
for handle in handles { let result = handle.await.unwrap(); println!("{}", result); }}実行結果:
ファイル1のダウンロードが完了しましたファイル2のダウンロードが完了しましたファイル3のダウンロードが完了しましたよく見てください。ファイル3の待ち時間がいちばん短いので、実際にはファイル3が最初に終わります。でも結果はハンドルを .await した順番で表示しているので、出力はいつも 1、2、3 の順になります。それでも、3つのダウンロードは同時に動いていました。
なぜ .unwrap() があるのでしょうか? JoinHandle を .await すると Result が返ってきます。タスクがパニックしているかもしれないからです。もしパニックしていたら、プログラム全体が落ちる代わりに Err を受け取れます。これは並行処理のページで見た thread::spawn と .join() によく似ていますが、それはわざとそうなっているのです。
よくある間違い
Section titled “よくある間違い”.await を忘れる。 Future は遅延評価されるので、.await を忘れるとその作業は実行されません。その値を使おうとすれば、コンパイラが止めてくれます。
async fn get_number() -> u32 { 5}
#[tokio::main]async fn main() { let n = get_number(); // .await を忘れた println!("{}", n + 1);}error[E0369]: cannot add `{integer}` to `impl Future<Output = u32>` --> src/main.rs:8:22 |8 | println!("{}", n + 1); | - ^ - {integer} | | | impl Future<Output = u32>このメッセージは、n が u32 ではなく「u32 を返す Future」なので、整数を足せないと言っています。直し方は let n = get_number().await; です。何も返さない関数をただ呼んだだけ(たとえば save_file();)の場合は、警告が出るだけです。だから警告はきちんと読みましょう。
warning: unused implementer of `Future` that must be used |7 | save_file(); | ^^^^^^^^^^^ | = note: futures do nothing unless you `.await` or poll themこの警告は「Future は .await するかポーリングしない限り何もしません」と教えてくれています。
非同期のコードの中で std::thread::sleep を使う。 これは問題なくコンパイルできてしまうので、気づきにくい間違いです。
async fn slow_task() { std::thread::sleep(std::time::Duration::from_secs(1)); // スレッド全体を止めてしまう!}std::thread::sleep はスレッド全体を止めてしまいます。ランタイムはほかのタスクに切り替えられないので、そのスレッドの上のものはすべて止まります。1つのテーブルの前で、ウェイターが立ったまま眠ってしまうようなものです。非同期のコードの中では、必ず代わりに tokio::time::sleep(...).await を使いましょう。std::fs で巨大なファイルを読むような、時間のかかるブロッキング処理も同じです。tokio には非同期版(tokio::fs)がありますし、tokio::task::spawn_blocking を使うこともできます。
async で CPU の計算が速くなると思う。 async が役に立つのは、タスクが待っているときです。画像のリサイズや大量の計算のように、タスクが忙しく働いているときには役に立ちません。ウェイターがたくさん注文を受けても、料理が速くできるわけではないのです。重い CPU の計算には、スレッド(または rayon のようなクレート)を使いましょう。
async とスレッド、どちらを使う?
Section titled “async とスレッド、どちらを使う?”| async を使うとき | スレッドを使うとき |
|---|---|
| ほとんどの時間を待つことに使う(ネットワーク、データベース、タイマー) | 重い計算をする |
| 何千ものタスクを同時に扱う必要がある(Web サーバーなど) | 並列に動かしたいものが少しだけ |
| reqwest や axum のような非同期クレートを使う | ランタイムへの依存を増やしたくない |
どちらか一方だけを選ぶ必要はありません。実際のプログラムでは、ネットワークには async、重い処理にはスレッドというように、両方を使うことがよくあります。
タスク同士でメッセージをやり取りしたいときは? tokio には専用のチャネル tokio::sync::mpsc があります。チャネルのページで紹介する標準ライブラリのチャネルと同じように使えますが、rx.recv().await と書くところが違います。そのため、メッセージを待っているあいだもスレッドは止まりません。
async で作られているもの
Section titled “async で作られているもの”Rust の非同期エコシステムのほとんどは tokio の上に作られています。あちこちで見かけるクレートを2つ紹介します。
- reqwest(英語):Web API を呼び出すための HTTP クライアントです。
reqwest::get(url).awaitと書くだけでページを取得できます。 - axum(英語):サーバーや API を作るための Web フレームワークで、tokio チームが作っています。
どちらも、JSON を読み書きする serde_json と相性抜群です。
1. 朝ごはんをいっぺんに作る
Section titled “1. 朝ごはんをいっぺんに作る”非同期関数を3つ書いてください。make_toast(30ms 待つ)、fry_eggs(20ms 待つ)、brew_coffee(10ms 待つ)です。それぞれ料理の名前を String で返します。3つを同時に動かして、できあがったものを表示しましょう。ヒント:tokio::join! は複数の Future を受け取り、その結果をタプルで返します。
解答を見る
use std::time::Duration;use tokio::time::sleep;
async fn make_toast() -> String { sleep(Duration::from_millis(30)).await; String::from("トースト")}
async fn fry_eggs() -> String { sleep(Duration::from_millis(20)).await; String::from("目玉焼き")}
async fn brew_coffee() -> String { sleep(Duration::from_millis(10)).await; String::from("コーヒー")}
#[tokio::main]async fn main() { let (toast, eggs, coffee) = tokio::join!(make_toast(), fry_eggs(), brew_coffee()); println!("朝ごはん: {}、{}、{}", toast, eggs, coffee);}実行結果:
朝ごはん: トースト、目玉焼き、コーヒー最初にできあがるのはコーヒーですが、タプルはいつも Future を並べた順番で返ってきます。朝ごはん全体にかかる時間は 60ms ではなく、いちばん遅い料理と同じ約 30ms です。
2. 裏で数を2乗する
Section titled “2. 裏で数を2乗する”10 * n ミリ秒待ってから n * n を返す async fn square_later(n: u64) -> u64 を書いてください。main では、1から4までの数それぞれにタスクを1つずつ起動し、すべてを待ってから合計を表示します。ヒント:tokio::spawn(...) のハンドルをそれぞれ Vec に入れ、ハンドルをループで回して handle.await.unwrap() を足していきます。
解答を見る
use std::time::Duration;use tokio::time::sleep;
async fn square_later(n: u64) -> u64 { sleep(Duration::from_millis(10 * n)).await; n * n}
#[tokio::main]async fn main() { let mut handles = Vec::new(); for n in 1..=4 { handles.push(tokio::spawn(square_later(n))); }
let mut total = 0; for handle in handles { total += handle.await.unwrap(); } println!("合計 = {total}");}実行結果:
合計 = 304つのタスクはすぐに動き出し、同時に待つので、かかる時間は 10 + 20 + 30 + 40 = 100ms ではなく約 40ms です。
- async を使うと、タスクが待っているあいだに別のタスクへ切り替えることで、1つのスレッドがたくさんのタスクをこなせます。
- 非同期のコードを動かすにはランタイムが必要です。
cargo add tokio --features fullで tokio を追加し、mainに#[tokio::main]を付けます。 async fnは Future を返します。Future は遅延評価されるので、.awaitするまで何も実行されません。- 複数の Future を同時に待つには
tokio::join!を、タスクを起動してJoinHandleを受け取るにはtokio::spawnを使います。 - 非同期のコードでは
std::thread::sleepを使わず、tokio::time::sleep(...).awaitを使いましょう。 - async は待ち時間のためのものです。重い CPU の計算にはスレッドを使いましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. async fn を .await せずに呼び出すと、どうなりますか?
2. #[tokio::main] は何をしますか?
3. 30ms と 20ms 待つ2つの Future があります。両方を tokio::join! で待つと、だいたいどれくらいかかりますか?
4. 非同期のコードの中で std::thread::sleep を使うと何が問題なのですか?
5. async がいちばん向いている仕事はどれですか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- Fundamentals of Asynchronous Programming — The Rust Book(英語):Future、
async、.await、ストリームについての公式の章です。日本語版の Rust Book にはまだ async の章がありません。 - tokio のチュートリアル(英語):小さな Redis 風のサーバーを作りながら、タスク、状態の共有、チャネルを実際に手を動かして学べるガイドです。
- docs.rs の tokio(英語):
tokio::spawn、tokio::time、tokio::syncなどの API リファレンスです。 - Asynchronous Programming in Rust(async book)(英語):Future とエグゼキュータ(executor)が内部でどう動いているのかを、さらに深く掘り下げています。