Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

11章のコードをActix-Web 2.0.0に移植する #2

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

KeenS
Copy link
Collaborator

@KeenS KeenS commented Dec 27, 2019

11章の著者のκeenです。実践Rust入門の出版後半年くらいでRustでの非同期のエコシステムに大きな変化があったのでフォローアップします。

このプルリクエストについて

主な目的は以下です。

  • サンプルコードをasync/awaitに対応したActix-Web 2.0.0 に移植したコードを見せる
  • コード内で可能な場所はasync/awaitを使って書き直したコードを見せる

現在のRustにはasync/await構文が入っており、扱いづらかった Future を自然な構文で書けるようになりました。また、使っているフレームワークもバージョンアップを重ねています。

そこで本文はもう変更できませんがせめてWeb上のリソースくらいはと思ってプルリクエストを作りました。マージしてしまうと本文との対応が取れなくなるのでこのプルリクエストはopenしたままにしておくと思います。

このプルリクエストの説明以外にもコード内に多少コメントを追加しています。実践Rust入門内の順番に合わせてstart-aw → static-files → templates → log-collectorの順に読むことを想定しています。

また、このプルリクエストはκeenが一人で書いて他の共著者や編集者のレビューを受けずに投稿しているので 文責はκeenのみにあります

時系列

11章に関係するRust界隈の動きを簡単にまとめます。

  • ~2019-04: (Rust 1.32.0がベース) 原稿締切。
  • 2019-05: 実践Rust入門出版。
  • 2019-06: (Rust 1.35.0) Actix-Web 1.0.0がリリースされた。アクターシステムに依存しなくなった。
  • 2019-07: (Rust 1.36.0) 標準ライブラリに Future が入った。
  • 2019-11: (Rust 1.39.0) async / await が安定化された 🎉 。
    Futures 0.3.0がリリースされた。標準ライブラリの Future と互換。
    Tokio 0.2.0がリリースされた。 async / await 対応。
    Async-StdというTokioの対抗馬のクレートの1.0.0がリリースされた。async/await 対応。
  • 2019-12: (Rust 1.39.0) Hyper 0.13.0がリリースされた。 async/await 対応。
  • 2019-12: (Rust 1.40.0) Actix-Web 2.0.0がリリースされた。async/await 対応。
  • 2020-01: (Rust 1.40.0) Reqwest 0.10.0がリリースされた。 async/await 対応。

Reqwestについては async/await 対応版の0.10.0がまだアルファステータスです。遠くないうちにReqwestでも async/await が使えるようになるでしょう。 追記 2020/1/2: 0.10.0がリリースされました /追記

非同期処理、Future、そしてasync/awaitについて軽く

コード内のコメントに解説や関連リソースへのリンクを張ったのでここでは軽い説明に留めます。

現代的なアプリケーションではIOなどの同期処理を非同期に扱いたいというモチベーションがあります。クライアントからリクエストを受けてデータベースに問い合わせて結果をクライアントに返すWebサーバを考えましょう。同期処理ではデータベースに問い合わせた結果が返ってくるまでサーバは何もせずに待ちます。他のクライアントが接続するのを待っていたとしてもです。

client2  client    server       DB
     |      | request  |        |
     |      |--------->| query  |
     |      |          |------->|
     |      |         >|        |
     |      |         >|        |
WAIT |      |    IDLE >|        | process
 .   |      |         >|        |
 .   |      |         >| result |
 .   |      |          |<-------|
     |      |          |        |
     |      | response |        |
     |      |<---------|        |
     |    request      |        |
     |---------------->|        |
          ....
          ....

これでは効率が悪いので、待ち時間に別の処理をしましょうというのが非同期処理です。

client2    client    server     DB
  |         | request  |        |
  |         |--------->|        |
  |         |          | query  |
  | request |          |------->|
  |------------------->|        |
  |         |          | query  |
  |         |          |------->| process
  |         |          |        |
  |         |          | result |
  |         |          |<-------|
  |         | response |        |
  |         |<---------| result |
  | response|          |<-------|
  |<-------------------|        |

こういう処理を愚直にやろうとすると、とても骨が折れます。1つの処理の間に別の処理をするということは処理を細切れにするということです。今まで1つの関数で書いていた処理が3つにも4つにも分かれ、それぞれ全然違う場所で実行されます。興味のある方は愚直なイベント駆動で書いたHello Worldを読んでみて下さい。コードを上から下に読んでも処理の流れが分からないなど、そのまま読み書きするにはいささか以上に厳しいコードになります。

そこで非同期処理を比較的まともに書くデザインパターンにFutureパターンというのがあります。Futureがあると少なくとも上から下に読めは処理の流れが分かるようになります。FutureパターンをRustで実現するための基盤が Future トレイトです。Futureパターンを使ったコードがどいうものかはFuture を使ったHello Worldを読んでみて下さい。先程よりはまともになっているかと思います。実践Rust入門執筆時点ではこの仕組みだけでコードを書いていました。

Future で上から下に読めるようにはなったのですが、それでもまだ普段のHello Worldとは大きく見た目が異なります。そこで Future に糖衣構文を被せ、見た目を普段書く形式(直接形式)に近い形でコードを書けるようにしたのが async/awaitです。どのくらい綺麗になったかはasync/await を使ったHello Worldを見ると分かりやすいです。async/await 構文を使えば直接形式と同じ見た目でプログラムを書けるようになるのです。

ということで async/await は何をしているのかというと、 Future を書きやすくしています。Future は何をしているかというと、イベント駆動な処理を書きやすくしています。

async/await の使い方

async/await の下地には Future があると説明しました。ということで asyncawaitFuture を使う挙動をします。直感的には asyncT -> Future<T>awaitFuture<T> -> T の動きをします。

async ブロック

async ブロックは無名の Future を実装した型を作ります。例えば async { 1 } と書くと1を返す無名の Future を作ります。

use std::future::Future;

fn async_one() -> impl Future<Output = i32> {
    async { 1 }
}

|| { 1 } と書くと呼ぶと1を返す無名のクロージャを作るのに似ていますね。

async fn

fn () -> impl Future<Output=T> { async { ... } } と書くのはあまりに定型文なので簡単に書けます。先程の例は以下のようにも書けます。ほとんど普通の関数と変わらない見た目ですね。

async fn async_one() -> i32 {
    1
}

await

awaitFuture<T> -> T のような動きをします。ただし async の中でしか使えません。例えば async_one の返り値を取り出そうとして以下のように書いてもコンパイルは通りません。

fn two() -> i32 {
    // Futureトレイトを実装した値に対して `.await` という構文で中身を取り出す。
    // 他言語とは見た目が違うので注意。
    let one = async_one().await;
    one + one
}

これは以下のようなコンパイルエラーになります。

error[E0728]: `await` is only allowed inside `async` functions and blocks
  --> async_hello.rs:11:15
   |
10 | fn two() -> i32 {
   |    --- this is not `async`
11 |     let one = async_one().await;
   |               ^^^^^^^^^^^^^^^^^ only allowed inside `async` functions and blocks

async fn (または async ブロック) の中でしか await を呼べないようになっています。このコードは以下のように async fn で書き換えてあげると動きます。

async fn async_two() -> i32 {
    let one = async_one().await;
    one + one
}

この構造、はじめてRustを触ったときの Resultoption から値が取り出せなくて悩んだ経験を思い出しませんか?だいたい Future もそういう類のものです。ResultOption はパターンマッチで剥がせますが Future を剥がす手段は基本的にはないです。最後まで Future をリレーしていって下さい。Future の中に入ったままフレームワーク(非同期ランタイム)に渡すことになります。今回のActix-WebでもAPIハンドラは async 関数を要求されていて、中身を取り出さなくてもプログラミングできるようになっています。最終的にはActix-Webの内部で中身が取り出されます。

Future は手作りしない

asycawait だけでは別にIO処理の間に他の処理をできるようになったりはしません。そういうものは非同期ライブラリに頼ります。例えばAsynt-Stdの Write::write を使えばファイルに書き出す処理を非同期にしてくれます。あとはそれを使うときに async の中で await してあげればいいのです。

ユーザは既存の async/await を使って非同期ライブラリを組み合わせるだけで、 Future トレイトを実装する型を作ることはほとんどありません。

非同期ランタイム

Future は「将来実行されるタスク」であって、実行までは含んでいません。これを(多くの場合複数)動かすのは Future とは別の仕組みになります。Future (= タスク)を1スレッド内で複数動かすには相応の仕組み(スケジューラ)を整える必要があります。これもユーザが手で書くことはほとんどなくて、スケジューラを提供してくれるライブラリを使います。そういうライブラリを非同期ランタイムと呼んだりします。

非同期ランタイムは有名どころだとTokio、Async-Std、Actix-RT、(一応)Futuresなどがあります。ランタイムが変わるとに Future の作り方が違うケースもあり、「このFutureはXXXライブラリのランタイムを使ったときにしか動かない」などの動作をするFutureがたまにあります。そのため、非同期ランタイムライブラリをを中心に非同期エコシステムが広がっています。非同期ランタイムを選ぶことはただ単に実行方法を選ぶだけでなく、エコシステム全体を選ぶことになっている場合もあるので注意して下さい。

大抵のライブラリはTokioで動いていますが、今回我々が使っているActix-WebはActix-RTで動いています。Actixのエコシステムに乗っかっている訳です。

注意点

特に他言語でFutureパターンを使ったことがある方向けに注意点を挙げておきます。

Futureはスレッドを使うことを含意しない

Futureを実行すると裏でスレッドが作られたりスレッドプールで処理が走るデザインもありますが、Rustの Future はそうではありません。処理を細切れにするのをサポートするだけです。別スレッドで実行したいなら「処理を別スレッドで実行して Future を返す」APIや 「Future を受け取って別スレッドで実行する」APIを提供するライブラリを別途使う必要があります。

Futureは勝手に実行されない

勝手に別スレッドで実行されないということとも繋がるのですが、Futureを作っただけでは実行されません。例えば以下のようなコードを書いたとしましょう。

// Futureを返す関数
async fn async_hello() {
    println!("Hello");
}

fn main() {
     // このFutureは実行されない
     async_hello();
}

この async_hello() が返す Future は実行されません。「main がすぐに終了してしまうから100msくらい待たないといけない」などの理由ではなく、何秒待っても実行されません。Futurepoll メソッド を呼ばないと実行されない仕組みになっているのです。

「じゃあ poll を呼べばいいじゃん」と思うかもしれませんが、 poll を手で呼ぶのは少し面倒な作りになっています。興味のある方は私が以前書いたブログ記事 を読んでみて下さい。ということでほとんどの場合で Future を最終的に実行するには poll を呼ぶ準備を整えてくれるライブラリ(=非同期ランタイム)を使うことになります。

denjiry added a commit to denjiry/rustbook that referenced this pull request Dec 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant