コンテンツにスキップ

Rustのasync/await

レストランを思い浮かべてください。並行処理のページでは、スレッドについて学びました。スレッドは、テーブルごとにウェイターを1人ずつ雇うようなものです。それでもお店は回りますが、ウェイターを雇うのはお金がかかりますし、ほとんどの時間は厨房の料理を待って立っているだけです。

async(非同期処理)は、1人のウェイターがたくさんの注文を受けるようなものです。ウェイターはテーブル1の注文を取って厨房に伝え、料理ができるのを待つあいだにテーブル2へ向かいます。誰もぼんやり立っていません。これが Rust の async と .await の考え方です。あるタスクが(ネットワークやファイル、タイマーを)待っているあいだに、別のタスクを動かすのです。

async が力を発揮するのは、プログラムが待ち時間の多い仕事をするときです。Web サーバー、API クライアント、チャットアプリ、ボットなどがそうです。このページでは、Rust でいちばん人気のある非同期ランタイム tokio を使います。

Rust の標準ライブラリには async と .await というキーワードはありますが、非同期のコードを実際に動かす「エンジン」は入っていません。このエンジンをランタイム(runtime)と呼び、ほとんどのプロジェクトでは tokio が使われています。新しいプロジェクトに追加してみましょう。

Terminal window
cargo new async_demo
cd async_demo
cargo add tokio --features full

すると、Cargo.toml に次の行が追加されます。

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

full フィーチャーを指定すると、すべての機能(タイマー、タスク、ネットワークなど)が有効になります。学んでいるうちは、これがいちばん手軽です。このページの「▶ 実際に試してみよう」ボタンも使えます。Rust Playground には tokio が最初から入っているからです。

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 は「少し待つ」の非同期版です。止まるのはこのタスクだけで、スレッド全体は止まりません。

ここが、多くの人が驚くところです。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 で済みます。本物のネットワーク通信では、この差はすぐに大きくなります。

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() によく似ていますが、それはわざとそうなっているのです。

.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 と書くところが違います。そのため、メッセージを待っているあいだもスレッドは止まりません。

Rust の非同期エコシステムのほとんどは tokio の上に作られています。あちこちで見かけるクレートを2つ紹介します。

  • reqwest(英語):Web API を呼び出すための HTTP クライアントです。reqwest::get(url).await と書くだけでページを取得できます。
  • axum(英語):サーバーや API を作るための Web フレームワークで、tokio チームが作っています。

どちらも、JSON を読み書きする serde_json と相性抜群です。

非同期関数を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 です。

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}");
}

実行結果:

合計 = 30
▶ 実際に試してみよう

4つのタスクはすぐに動き出し、同時に待つので、かかる時間は 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 の計算にはスレッドを使いましょう。

理解度チェック

1. async fn を .await せずに呼び出すと、どうなりますか?

2. #[tokio::main] は何をしますか?

3. 30ms と 20ms 待つ2つの Future があります。両方を tokio::join! で待つと、だいたいどれくらいかかりますか?

4. 非同期のコードの中で std::thread::sleep を使うと何が問題なのですか?

5. async がいちばん向いている仕事はどれですか?

スコア: 0 / 5