11章のコードをActix-Web 2.0.0に移植する #2
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
11章の著者のκeenです。実践Rust入門の出版後半年くらいでRustでの非同期のエコシステムに大きな変化があったのでフォローアップします。
このプルリクエストについて
主な目的は以下です。
現在のRustにはasync/await構文が入っており、扱いづらかった
Futureを自然な構文で書けるようになりました。また、使っているフレームワークもバージョンアップを重ねています。そこで本文はもう変更できませんがせめてWeb上のリソースくらいはと思ってプルリクエストを作りました。マージしてしまうと本文との対応が取れなくなるのでこのプルリクエストはopenしたままにしておくと思います。
このプルリクエストの説明以外にもコード内に多少コメントを追加しています。実践Rust入門内の順番に合わせてstart-aw → static-files → templates → log-collectorの順に読むことを想定しています。
また、このプルリクエストはκeenが一人で書いて他の共著者や編集者のレビューを受けずに投稿しているので 文責はκeenのみにあります 。
時系列
11章に関係するRust界隈の動きを簡単にまとめます。
Futureが入った。async/awaitが安定化された 🎉 。Futures 0.3.0がリリースされた。標準ライブラリの
Futureと互換。Tokio 0.2.0がリリースされた。
async/await対応。Async-StdというTokioの対抗馬のクレートの1.0.0がリリースされた。
async/await対応。async/await対応。async/await対応。async/await対応。Reqwestについては追記 2020/1/2: 0.10.0がリリースされました /追記async/await対応版の0.10.0がまだアルファステータスです。遠くないうちにReqwestでもasync/awaitが使えるようになるでしょう。非同期処理、Future、そしてasync/awaitについて軽く
コード内のコメントに解説や関連リソースへのリンクを張ったのでここでは軽い説明に留めます。
現代的なアプリケーションではIOなどの同期処理を非同期に扱いたいというモチベーションがあります。クライアントからリクエストを受けてデータベースに問い合わせて結果をクライアントに返すWebサーバを考えましょう。同期処理ではデータベースに問い合わせた結果が返ってくるまでサーバは何もせずに待ちます。他のクライアントが接続するのを待っていたとしてもです。
これでは効率が悪いので、待ち時間に別の処理をしましょうというのが非同期処理です。
こういう処理を愚直にやろうとすると、とても骨が折れます。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があると説明しました。ということでasyncとawaitはFutureを使う挙動をします。直感的にはasyncがT -> Future<T>、awaitがFuture<T> -> Tの動きをします。asyncブロックasyncブロックは無名のFutureを実装した型を作ります。例えばasync { 1 }と書くと1を返す無名のFutureを作ります。|| { 1 }と書くと呼ぶと1を返す無名のクロージャを作るのに似ていますね。async fnfn () -> impl Future<Output=T> { async { ... } }と書くのはあまりに定型文なので簡単に書けます。先程の例は以下のようにも書けます。ほとんど普通の関数と変わらない見た目ですね。awaitawaitはFuture<T> -> Tのような動きをします。ただしasyncの中でしか使えません。例えばasync_oneの返り値を取り出そうとして以下のように書いてもコンパイルは通りません。これは以下のようなコンパイルエラーになります。
async fn(またはasyncブロック) の中でしかawaitを呼べないようになっています。このコードは以下のようにasync fnで書き換えてあげると動きます。この構造、はじめてRustを触ったときの
Resultやoptionから値が取り出せなくて悩んだ経験を思い出しませんか?だいたいFutureもそういう類のものです。ResultやOptionはパターンマッチで剥がせますがFutureを剥がす手段は基本的にはないです。最後までFutureをリレーしていって下さい。Futureの中に入ったままフレームワーク(非同期ランタイム)に渡すことになります。今回のActix-WebでもAPIハンドラはasync関数を要求されていて、中身を取り出さなくてもプログラミングできるようになっています。最終的にはActix-Webの内部で中身が取り出されます。Futureは手作りしないasycとawaitだけでは別に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を作っただけでは実行されません。例えば以下のようなコードを書いたとしましょう。
この
async_hello()が返すFutureは実行されません。「mainがすぐに終了してしまうから100msくらい待たないといけない」などの理由ではなく、何秒待っても実行されません。Futureはpollメソッド を呼ばないと実行されない仕組みになっているのです。「じゃあ
pollを呼べばいいじゃん」と思うかもしれませんが、pollを手で呼ぶのは少し面倒な作りになっています。興味のある方は私が以前書いたブログ記事 を読んでみて下さい。ということでほとんどの場合でFutureを最終的に実行するにはpollを呼ぶ準備を整えてくれるライブラリ(=非同期ランタイム)を使うことになります。