Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Futureと非同期構文

Rustにおける非同期プログラミングの主要な要素は、future と、Rustの async および await キーワードです。

future とは、今は準備できていないかもしれないものの、将来のある時点で準備できる値のことです。(この同じ概念は多くの言語に見られ、taskpromise など別の名前で呼ばれることもあります。)Rustは、共通のインターフェイスを持ちながら異なるデータ構造でさまざまな非同期操作を実装できるようにするための構成要素として、Future トレイトを提供しています。Rustでは、futureとは Future トレイトを実装する型のことです。各futureは、これまでにどこまで進んだか、そして「ready」が何を意味するかについての独自の情報を保持しています。

ブロックや関数に async キーワードを適用すると、それらが中断および再開可能であることを指定できます。asyncブロックまたはasync関数の内部では、await キーワードを使って futureを待機する(つまり、それがreadyになるまで待つ)ことができます。asyncブロックまたは関数の中でfutureを待機する箇所はどこでも、そのブロックや関数が一時停止および再開する可能性のある地点です。futureに対して、その値がまだ利用可能かどうかを確認する処理は ポーリング と呼ばれます。

C#やJavaScriptなどの他の言語でも、非同期プログラミングのために asyncawait キーワードを使います。これらの言語に慣れているなら、Rustがこの構文を扱う方法にかなり大きな違いがあることに気づくかもしれません。それには、これから見るように、ちゃんとした理由があります。

非同期Rustを書くとき、私たちはほとんどの場合 asyncawait キーワードを使います。Rustは、それらを Future トレイトを使った等価なコードにコンパイルします。これは、for ループを Iterator トレイトを使った等価なコードにコンパイルするのと同じです。ただし、Rustは Future トレイトを提供しているので、必要に応じて自分自身のデータ型に対してそれを実装することもできます。この章を通して見ていく関数の多くは、独自の Future 実装を持つ型を返します。章の終わりにトレイトの定義へ戻り、その仕組みをさらに掘り下げますが、先に進むにはこの程度の詳細で十分です。

ここまでの話は少し抽象的に感じられるかもしれないので、最初の非同期プログラムを書いてみましょう。小さなWebスクレイパーです。コマンドラインから2つのURLを受け取り、その両方を並行して取得し、先に完了した方の結果を返します。この例にはかなり多くの新しい構文が出てきますが、心配はいりません。必要なことは進みながらすべて説明します。

最初の非同期プログラム

この章では、エコシステムのさまざまな要素をやりくりすることではなく、非同期を学ぶことに集中できるように、私たちは trpl クレート(trpl は “The Rust Programming Language” の略です)を作成しました。これは、主に futures クレートと tokio クレートから、この章で必要になる型、トレイト、関数をすべて再エクスポートしています。futures クレートは、Rustにおける非同期コード実験のための公式な場であり、実際に Future トレイトが最初に設計された場所でもあります。Tokioは、今日のRustで最も広く使われている非同期ランタイムで、とりわけWebアプリケーションでよく使われています。他にも優れたランタイムはありますし、用途によってはそちらの方が適しているかもしれません。trpl では内部的に tokio クレートを使っていますが、それは十分にテストされていて広く使われているからです。

場合によっては、trpl はこの章に関係する詳細に集中できるよう、元のAPIに名前を付け直したりラップしたりもしています。このクレートが何をしているのかを理解したければ、そのソースコードを確認することをおすすめします。各再エクスポートがどのクレートから来ているのかを見ることができますし、このクレートが何をしているかを説明する豊富なコメントも残してあります。

hello-async という名前の新しいバイナリプロジェクトを作成し、依存関係として trpl クレートを追加してください。

$ cargo new hello-async
$ cd hello-async
$ cargo add trpl

これで、trpl が提供するさまざまな部品を使って最初の非同期プログラムを書けます。2つのWebページを取得し、それぞれから <title> 要素を取り出し、その一連の処理全体を先に終えたページのタイトルを表示する小さなコマンドラインツールを作ります。

page_title関数の定義

まずは、1つのページURLを引数に取り、それにリクエストを行い、<title> 要素のテキストを返す関数を書いてみましょう(リスト17-1を参照)。

extern crate trpl; // required for mdbook test

fn main() {
    // TODO: we'll add this next!
}

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

まず、page_title という名前の関数を定義し、それに async キーワードを付けます。次に、trpl::get 関数を使って渡されたURLを取得し、await キーワードを付けてレスポンスを待機します。response のテキストを取得するために、その text メソッドを呼び出し、もう一度 await キーワードで待機します。これら2つのステップはいずれも非同期です。get 関数では、サーバーがレスポンスの最初の部分を送り返してくるのを待たなければなりません。この部分にはHTTPヘッダーやクッキーなどが含まれており、レスポンスボディとは別に届くことがあります。特にボディが非常に大きい場合には、すべてが到着するまでに時間がかかることがあります。レスポンスの 全体 が届くまで待つ必要があるため、text メソッドもasyncです。

これら2つのfutureはどちらも明示的に待機しなければなりません。なぜなら、Rustのfutureは 遅延的 だからです。await キーワードで求めるまで、何もしません。(実際、futureを使わないと、Rustはコンパイラ警告を表示します。)これは、第13章の「イテレータで一連の要素を処理する」節でのイテレータの説明を思い出させるかもしれません。イテレータは、next メソッドを呼び出さない限り何もしません。直接呼び出す場合でも、内部で next を使う for ループや map のようなメソッドを通じて呼び出す場合でも同じです。同様に、futureも明示的に求めない限り何もしません。この遅延性によって、Rustは実際に必要になるまで非同期コードを実行しないで済みます。

注: これは、第16章の「spawnで新しいスレッドを作る」 節で thread::spawn を使ったときに見た挙動とは異なります。そこでは、 別のスレッドに渡したクロージャがすぐに実行を開始しました。また、 多くの他の言語における非同期へのアプローチとも異なります。しかし、 イテレータの場合と同じく、Rustがその性能保証を提供できるようにする ためには、これが重要なのです。 response_text を取得したら、Html::parse を使ってそれを Html 型のインスタンスにパースできます。これで、生の文字列ではなく、より豊かな データ構造として HTML を扱うためのデータ型が手に入ります。特に、 select_first メソッドを使うと、指定した CSS セレクタに一致する最初の 要素を見つけられます。文字列 "title" を渡すと、ドキュメント内に存在す れば最初の <title> 要素を取得できます。一致する要素が存在しない可能性が あるため、select_firstOption<ElementRef> を返します。最後に、 Option::map メソッドを使います。これにより、Option に値が存在するとき はその項目を処理し、存在しないときは何もしないようにできます。(ここでは match 式を使うこともできますが、map の方がより慣用的です。)map に 渡す関数の本体では、title に対して inner_html を呼び出してその内容を 取得します。これは String です。最終的に得られるのは Option<String> です。

Rust の await キーワードは、待機する式の 後ろ に置かれ、前には置かれ ません。つまり、これは 後置 のキーワードです。他の言語で async を使っ たことがあるなら、これは慣れているものと異なるかもしれませんが、Rust で はこのおかげでメソッドチェーンがずっと扱いやすくなります。その結果、 リスト17-2に示すように、page_title の本体を変更して、trpl::gettext の呼び出しを、その間に await を挟みながら連結できます。

extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    // TODO: we'll add this next!
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

これで、最初の async 関数をうまく書くことができました! main にそれを呼 び出すコードを追加する前に、ここまでに書いたものと、それが何を意味するの かについてもう少し見ていきましょう。

Rust は async キーワードの付いた ブロック を見ると、それを Future トレイトを実装する固有の無名データ型にコンパイルします。Rust は async の付いた 関数 を見ると、その本体が async ブロックである非 async 関数に コンパイルします。async 関数の戻り値の型は、コンパイラがその async ブロッ クのために生成する無名データ型の型です。

したがって、async fn と書くことは、戻り値型に対応する future を返す 関数を書くことと等価です。コンパイラから見ると、リスト17-1の async fn page_title のような関数定義は、おおよそ次のような非 async 関 数定義と等価です。

#![allow(unused)]
fn main() {
extern crate trpl; // mdbook のテストに必要
use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}
}

変換後のバージョンの各部分を順に見ていきましょう。

  • 第10章の 「パラメータとしてのトレイト」 節で説明した impl Trait 構文を使っています。
  • 返される値は Future トレイトを実装しており、その関連型は Output で す。Output 型が Option<String> であることに注目してください。これ は、page_titleasync fn バージョンにおける元の戻り値型と同じで す。
  • 元の関数本体で呼び出されていたコードはすべて、async move ブロックで 包まれています。ブロックは式であることを思い出してください。このブロッ ク全体が、その関数から返される式です。
  • この async ブロックは、先ほど説明したとおり Option<String> 型の値を 生成します。その値は、戻り値型にある Output 型と一致します。これは、 これまでに見てきた他のブロックと同じです。
  • 新しい関数本体が async move ブロックになっているのは、url パラメー タの使い方によるものです。(asyncasync move の違いについては、 この章の後の方でさらに詳しく扱います。)

これで、main から page_title を呼び出せるようになります。

ランタイムで async 関数を実行する

まずは、リスト17-3に示すように、1つのページのタイトルを取得します。残念 ながら、このコードはまだコンパイルできません。

extern crate trpl; // required for mdbook test

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

コマンドライン引数を取得するために、第12章の 「コマンドライン引数を受け取る」 節で使ったの と同じパターンに従います。次に、その URL 引数を page_title に渡し、結 果を await します。future が生成する値は Option<String> なので、その ページに <title> があったかどうかに応じて異なるメッセージを表示するた めに、match 式を使います。

await キーワードを使える場所は async 関数または async ブロックの中だけ であり、Rust では特別な main 関数を async としてマークできません。

error[E0752]: `main` function is not allowed to be `async`
 --> src/main.rs:6:1
  |
6 | async fn main() {
  | ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

mainasync としてマークできない理由は、async コードには runtime、つまり非同期コード実行の詳細を管理する Rust クレートが必要だ からです。プログラムの main 関数はランタイムを 初期化 することはでき ますが、それ自体がランタイム そのもの ではありません。(なぜそうなのか については、少し後でさらに見ていきます。)async コードを実行する Rust プ ログラムにはどれにも、future を実行するランタイムをセットアップする場所が 少なくとも1つあります。

async をサポートするほとんどの言語はランタイムを同梱していますが、Rust はそうではありません。その代わりに、さまざまな async ランタイムが利用で き、それぞれが対象とするユースケースに適した異なるトレードオフを持ってい ます。たとえば、多数の CPU コアと大量の RAM を備えた高スループットの Web サーバーは、単一コアで RAM が少なく、ヒープ確保もできないマイクロコント ローラとはまったく異なる要件を持っています。そうしたランタイムを提供する クレートは、ファイル I/O やネットワーク I/O のような一般的機能の async 版もよく提供しています。

ここでは、この章の残りを通して trpl クレートの block_on 関数を使いま す。この関数は future を引数に取り、その future が完了まで実行されるまで 現在のスレッドをブロックします。内部では、block_on を呼び出すと、渡さ れた future を実行するために tokio クレートを使ったランタイムがセット アップされます(trpl クレートの block_on の振る舞いは、他のランタイ ムクレートの block_on 関数と似ています)。future が完了すると、 block_on はその future が生成した値をそのまま返します。 page_title が返す Future を block_on に直接渡し、 それが完了したら、リスト17-3でやろうとしたように、結果として得られる Option<String> に対して match することもできます。しかし、この章の ほとんどの例では(そして実際の世界のほとんどの async コードでも)、 単なる 1 回の async 関数呼び出し以上のことを行うので、代わりに async ブロックを渡し、リスト17-4のように page_title 呼び出しの 結果を明示的に await します。

extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

このコードを実行すると、最初に期待していた動作が得られます。

$ cargo run -- "https://www.rust-lang.org"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
            Rust Programming Language

ふう、ようやく動く async コードができました! しかし、2 つのサイトを 競争させるコードを追加する前に、Future がどのように動くのかに少しだけ 注意を戻しましょう。

await ポイント、つまりコードが await キーワードを使うすべての場所は、制御がランタイムに返される地点を表します。 これを可能にするために、Rust は async ブロックに関わる状態を追跡しておく 必要があります。そうすることで、ランタイムは別の作業を開始し、最初の処理を もう一度進められる準備ができたときにそこへ戻ってこられます。これは、 各 await ポイントで現在の状態を保存するために、次のような enum を 書いたかのような、目に見えない状態機械です。

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test

enum PageTitleFuture<'a> {
    Initial { url: &'a str },
    GetAwaitPoint { url: &'a str },
    TextAwaitPoint { response: trpl::Response },
}
}

しかし、各状態の間を遷移させるコードを手で書くのは面倒で、 間違いも起こりやすくなります。特に、あとでコードにさらに多くの機能や 状態を追加する必要がある場合はなおさらです。幸い、Rust コンパイラは async コード用の状態機械データ構造を自動的に作成して管理してくれます。 データ構造に関する通常の借用規則と所有権規則は引き続きすべて適用されますが、 うれしいことに、コンパイラはそれらのチェックも私たちの代わりに行い、 役に立つエラーメッセージも提供してくれます。この章の後半で、 そのいくつかを見ていきます。

最終的には、この状態機械を実行する何かが必要であり、それが ランタイムです。(ランタイムについて調べていると エグゼキュータ という 言葉を目にすることがあるのはこのためです。エグゼキュータは、async コードを 実行する責任を持つランタイムの一部です。)

これで、リスト17-3でコンパイラが main 自体を async 関数にすることを 止めた理由がわかるでしょう。もし main が async 関数だったら、 main が返す Future が何であれ、その状態機械を別の何かが管理する必要が あります。しかし main はプログラムの開始地点です! その代わりに、 main の中で trpl::block_on 関数を呼び出してランタイムを設定し、 async ブロックが返す Future を完了するまで実行しました。

注: 一部のランタイムはマクロを提供しているので、async main 関数を実際に書くこともできます。そうしたマクロは async fn main() { ... } を通常の fn main に書き換えます。これは、リスト17-4で手作業で行ったのと 同じことです。つまり、trpl::block_on のように Future を完了まで実行する 関数を呼び出します。

では、これらの要素をまとめて、どのように並行コードを書けるかを見ていきましょう。

2つの URL を並行に競争させる

リスト17-5では、コマンドラインから渡された 2 つの異なる URL に対して page_title を呼び出し、先に完了した方の Future を選ぶことで それらを競争させます。

extern crate trpl; // required for mdbook test

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::select(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title was: '{title}'"),
            None => println!("It had no title."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let response_text = trpl::get(url).await.text().await;
    let title = Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}

まず、ユーザーから渡された各 URL に対して page_title を呼び出します。 その結果得られる Future を title_fut_1title_fut_2 として保存します。 覚えておいてください。Future は遅延評価であり、まだ await していないので、 この時点ではまだ何も行いません。次に、それらの Future を trpl::select に 渡します。これは、渡された Future のうちどれが最初に完了したかを示す値を 返します。

注: 内部的には、trpl::selectfutures クレートで定義されている、 より汎用的な select 関数の上に構築されています。futures クレートの select 関数は trpl::select 関数にはできない多くのことを行えますが、 今は飛ばしてよい追加の複雑さもいくつかあります。

どちらの Future が「勝って」もまったく自然なので、Result を返すのは 適切ではありません。代わりに、trpl::select はまだ見たことのない型 trpl::Either を返します。Either 型は、2 つのケースを持つという点で Result に少し似ています。しかし、Result とは違って、Either には 成功や失敗という概念は組み込まれていません。代わりに、LeftRight を 使って「どちらか一方」を示します。

#![allow(unused)]
fn main() {
enum Either<A, B> {
    Left(A),
    Right(B),
}
}

select 関数は、最初の引数が勝った場合にはその Future の出力とともに Left を返し、そちら が勝った場合には 2 番目の Future 引数の出力とともに Right を返します。これは、関数を呼び出すときに引数が現れる順序と 一致しています。最初の引数は、2 番目の引数の左側にあります。

また、page_title を更新して、渡されたのと同じ URL を返すようにします。 そうすれば、先に返ってきたページに取得できる <title> がなかったとしても、 意味のあるメッセージを表示できます。その情報が使えるようになったので、 最後に println! の出力を更新し、どの URL が最初に完了したのかと、 その URL の Web ページにどのような <title> があるのか(あれば)を 示すようにします。

これで、小さくても動く Web スクレイパーができました! いくつか URL を選んで、 このコマンドラインツールを実行してみてください。あるサイトは他のサイトよりも 一貫して速いこともあれば、別の場合には、どのサイトが速いかが実行のたびに 変わることもあるでしょう。さらに重要なのは、Future を扱うための基本を 学んだので、これで async で何ができるのかをさらに深く掘り下げられることです。