RFC-2394: async_await
#rust #RFC #RFC-2394 #非同期 #翻訳 #既読
オリジナルURL: https://github.com/rust-lang/rfcs/blob/master/text/2394-async_await.md
注意: 正確性は重視していない意訳なので、正確に知りたい人は原文を参照のこと
関連RFC
RFC-2349: Pin構造体とUnpinトレイト
RFC-2418: futuresのAPIを標準ライブラリに導入
------
開始日: 2018-03-30
RFC PR: https://github.com/rust-lang/rfcs/pull/2394
Rust Issue: https://github.com/rust-lang/rust/issues/50547
1. 要約
futuresを用いたコードを書きやすくするために、asyncおよびawait構文を追加する
これは、futures用の小さなAPIをlibstdおよびlibcoreに追加するための関連するRFCを有する
(訳注) おそらくrfcs/pull/2395のこと
(訳注) ただし、これはrfcs/pull/2418で置き換えられている
2. 動機
ハイパフォーマンスなネットワークサーバを書く場合には、非同期I/Oが多用される
ただ、現状のRustだと非同期I/Oコードが書き難いので、もっと改善したい
Rustの非同期I/Oの歴史:
1.0より前: グリーンスレッド => 全てのランタイムに組み込むには重いので削除
1.0より後:
最初はmioが登場 (クロスプラットフォームなepoll的なcrate)
2016年の半ばにfuturesが登場
非同期操作を抽象的に表現するためのcrateで、Rustの非同期I/Oモデルの中核となる
mioを使って、futuresで表現された非同期I/Oを実行するためのtokioも同時期に登場
futuresベースのエコシステムから得られた経験やユーザフィードバック:
futuresを使っても、やっぱり非同期処理を手で書くのは大変
ただ幸運なことに、future抽象自体はRustに適している
=> 適切な糖衣構文さえあれば良さそう
=> 他の言語でも良く使われているasync/awaitを導入しよう
async/awaitとfuturesの組み合わせは強力
非同期I/Oだけではなく、より汎用的な非同期ないし並行抽象を表現するために利用可能
3. ガイドレベルの説明
3-1. async関数
asyncキーワードを付与すると"非同期関数"になる:
code:rust
async fn function(argument: &str) -> usize {
// ...
}
非同期関数は呼び出されてもすぐには実行されず、代わりにFutureトレイトを実装した無名型(のインスタンス)を返す。
そのfutureがポールされると、その関数は「次のawaitないしリターンポイント」まで評価される(詳細は「awaitの展開」の節を参照)。
非同期関数は遅延計算の一種で、その関数によって返されたfutureをポーリングするまでは、実際の処理は一切は走らない。
以下はその例:
code:rust
async fn print_async() {
println!("Hello from print_async")
}
fn main() {
let future = print_async();
println!("Hello from main"); // "Hello from main"が表示
futures::block_on(future); // "Hello from print_async"が表示
}
async fn foo(args..) -> Tは、実体としてはfn (args..) -> impl Future<Output = T>型の関数で、返り値の型(無名)はコンパイラによって自動的に生成される。
3-1-1. async ||クロージャ
asyncはクロージャとも組み合わせられるよ、というだけの話。
code:rust
fn main() {
let closure = async || {
println!("Hello from async closure.");
};
println!("Hello from main"); // "Hello from main"が出力
let future = closure();
println!("Hello from main again"); // "Hello from main again"が出力
futures::block_on(future): // "Hello from async closure."が出力
}
moveを付けて、変数群を捕捉することも可能。
3-2. ayncブロック
asyncブロックを使うことで、式からfutureを直接生成することも可能:
code:rust
let my_future = async {
printlN!("Hello from an async block");
};
機能的には、即座に呼び出されるasyncクロージャとほぼ等価。
code:rust
async { /* body */ }
// is equivalent to
(async || { /* body /* })()
ただし前者のbodyの中ではreturn、break、continueといった制御フロー構造は使用不可(body内に、別のループやクロージャがあり、その中で使用するのは良い)。
asyncブロック内で?演算子がどう扱われるべきか、は未解決の疑問。
asyncブロックにもmoveは指定可能。
3-3. コンパイラ組み込みのawait!マクロ
await!:
コンパイラに組み込みのマクロ
futureの計算を"停止"するために利用でき、制御を呼び出し元に委譲(yield)する
引数としてIntoFutureを実装した式を受け取り、それを結果の値へと評価する
code:rust
// future: impl Future<Output = usize>
let n = await!(future);
上のawaitを展開すると、futureのpollメソッドが繰り返し呼び出されており、それがPoll::Pendingを返している間は関数の制御が呼び出し元に委譲され、最終的には(Poll::Readyが返された場合には)結果の値へと評価される。
await!は、async関数/クロージャ/ブロック、の中でのみ使用可能。
将来的には、専用の構文になるかもしれないけれど、未解決の疑問があるので、まだ組み込みマクロ扱いにしている。
4. リファレンスレベルの説明
4-1. キーワード
asyncとawaitは2018エディションでキーワードになります。
4-2. async関数、クロージャ、ブロックの返り値
async関数の返り値の型は、コンパイラによって生成されるユニークな無名型(クロージャの型と似ている)
これはenumの一種と考えることも可能:
関数内の全ての"yield point"に、対応するvariantが存在する
"yield point": 関数の開始点、await式、return呼び出し
各variantには、その地点から制御を再開するために必要な情報(状態)、を保持する
関数が呼び出されると、全ての引数を含んだ初期状態、が返される
4-2-1. トレイト境界
この無名型はFutureトレイトを実装する:
返り値の型はFuture::Outputで表現
ポーリングによって、関数の状態が進む
awaitポイントに到達した場合にはPoll::Pendingが返される
returnポイントに到達した場合にはPoll::Readyが返される
既にPoll::Readyが返された後のポーリングは、panicとなる
この無名型は、Unpinトレイトに対するネガティブ実装を有する(つまりimpl !Unpin)。
理由: このfutureは内部参照を持つ可能性があり、それは決してmoveされる必要がないことを意味するため
4-3. 無名futureに捕捉されるライフタイム
async関数に対する全ての入力ライフタイムは、結果のfutureによって捕捉される(全ての引数が、少なくとも、futureの初期状態によって保持されるため)。
code:rust
async fn foo(arg: &str) -> usize { ... }
↑は↓と等しい。
code:rust
fn foo<'a>(arg: &'a str) -> impl Future<Output = usize> + 'a { ... }
これはデフォルトのimpl Traitの挙動とは異なる(こっちはライフタイムを捕捉しない)。
これは、async関数の表記上の返り値の型が、Tであってimpl Future<Output = T>ではない、大きな理由の一つである(詳細は後述)。
4-3-1. "initialization"パターン
async関数の引数の一部は、初期化処理にだけ使用し、返り値のfutureには含めたくないことがある
無駄に厳しいライムタイムを制約を回避するため
async関数では、この"initialization"パターンを表現することは不可能
ただし、以下のような方法で実現は可能
code:rust
// arg1のライフタイムだけが、返り値のfutureによって捕捉される
fn foo<'a>(arg1: &'a str, arg2: &str) -> impl Future<Output = usize> + 'a {
// arg2を使って、何らかの初期化処理を行う
// arg1のみを含んだasyncブロック
async move {
// この関数の非同期部分
}
}
4-4. awaitの展開
await!は、雑に言えば、次のように展開される:
code:rust
let mut future = IntoFuture::into_future($expression);
let mut pin = unsafe { Pin::new_unchecked(&mut future) };
loop {
match Future::poll(Pin::borrow(&mut pin), &mut ctx) {
Poll::Ready(item) => break item,
Poll::Pending => yield,
}
}
これは本来の展開とは異なる。
何故なら、yieldの概念は、async関数内の表層の構文では表現できないため。
これが、await!が実際のマクロではなく、コンパイラ組み込みとなっている理由である。
4-5. asyncとmoveの順序
混乱を避けるために順序はasync moveに固定。
code:rust
async move {
// body
}
5. 欠点
一行要約: 大きな機能追加だから導入が大変だし、入れてしまうともう後戻りができなくなってしまう
async/await構文の追加は、1.0リリース以来で、言語に対する最も大きな変更
機能単位を細かく区切って慎重に進めていく
私たちは、書きやすい非同期I/Oソリューションは、高パフォーマンスなネットワークサービスの分野でのRustの成功を支えるために不可欠だと信じている
2018年のゴールの一つ
Futureトレイトに基づくasync/await構文は、近い将来にこの目的を達成するための、最も好都合でリスクの少ないパス
このRFCおよび関連するrfcs/pull/2395は、以前のものに比べてよりfuturesおよびasync/awaitに強固にコミットしている
もしこれらの安定化後に、後戻りすることになったら、それはかなりコスト高
非同期プログラミング用の他のメカニズムを追加することも同様
ただしfuturesでの経験により、私たちは正しい道を進んでいると自信を持っている
他にも、このRFCでの選択に纏わる細かい欠点はいくつかある
その詳細と選択の妥当性は次節で取り上げる
6. 妥当性と代替案
6-1. 返り値 (impl Future<Output = T>ではなくT)
非同期関数の返り値の型をどうするかは、複雑な問題
二種類の型が考えれる:
内部返り値型: returnで指定された型
外部返り値型: 関数呼び出し時に、実際に呼び出し元に返される型
どちらも利点と欠点があって、多くの言語では"外部"の方が採用されているけれど、Rustは"内部"の方を選択した
6-1-1. ライフタイム省略問題
外部返り値型を採用した場合の問題の話:
デフォルトではimpl Traitは、入力引数のライフタイムを捕捉しない
ただし、async関数が返すfutureは、全ての入力引数を捕捉し、ライフタイムに関しても同様
それを正確に反映しようとすると、async関数ではライムタイム省略が行えず、以下のように書く必要が出てくる
code:rust
async fn foo<'ret, 'a: 'ret, 'b: 'ret>(x: &'a i32, y: &'b i32) -> impl Future<Output = i32> + 'ret {
*x + *y
}
これは明らかに煩雑なので避けたい(内部型を選択した大きな理由の一つ)。
async関数の場合にだけimpl Traitのデフォルト挙動を変化させる、という方法も可能ではあるが、それは内部型を用いるよりも筋が悪そう。
6-1-2. 多様な返り値
C#の開発者によると非同期関数の返り値にTask<T>(C#での外部型)を用いる主要な理由は「Task以外の型を返したいことがあるから」らしい。
このユースケースは、Rustにはあまり当てはまらない:
(1) futuresの0.2ブランチでは、FutureとStableFutureという別々のトレイトがありはする
ただし、これはobject-safe custom self-typesが安定版で利用可能になるまでの一時的な処置
(2) 現在の#[async]マクロは(boxed)変種を持っている
基本的には、全ての関数はunboxedな値を返すことを推奨している (box化が必要なら呼び出し元で)
(boxed)属性の目的は、オブジェクトセーフなトレイトでも非同期メソッドをサポートすること
これはオブジェクトセーフなトレイトでimpl Traitをサポートすることの特殊なケース
これはaysnc関数とは別に要求される機能
(3) ストリームを返すasync関数のサポートが提案されてはいる
ただし、これは返り値がfutureの場合とstreamの場合とで、関数内部の意味論が大幅に変わることを意味する
「未解決の疑問」の節で議論されているように、ジェネレータおよび非同期ジェネレータに基づく解法の方が筋が良さそう
以上の理由から、多様性(polymorphism)の観点から外部型を採用する大きな論拠はないものと考える。
6-1-3. 学習容易さ/ドキュメントのトレードオフ
利用者の観点からすれば、外部型の方がAPIドキュメントを見た際には直感的
実装者の観点からすれば、内部型の方が「どのような非同期関数を実装すべきか」が分かりやすい
関数内のreturnに渡した値の型と関数の(表示上の)返り値の型が一致するため
前者の方は、rustdocレベルでカバー可能そう:
目立つようにasync表記を付与したり、必要に応じて外部型を表示したり
具体的に、async関数をAPIドキュメントでどのように扱うか、は未解決の疑問として残っている
6-2. ジェネレータでのマクロではなく組み込み構文にする
組み込み構文の導入ではなく、「手続きマクロとジェネレータの安定化」に集中するのはどうか?
async関数は、()を委譲(yield)するジェネレータとしてモデル化することも可能
長期的に見れば、async関数用に専用構文を用意した方が良いと信じている
その方が書き易いし、利用頻度も十分多いはず
問題は、短期的に「ジェネレータの使用」と「async関数の導入」のどちらの方が安定化までの時間が短くて済むか
ジェネレータと手続きマクロは、それぞれ考えることが多いので、そう簡単には安定化されなさそう
=> それを待つより、最小限の組み込みasync/await機能の導入を優先した方が、おそらく安定化は早い
6-3. ジェネレータのみに基づくasync
別のデザインとして「async関数をジェネレータを生成するための構文」として扱うことも考えられる。
このデザインでは、おそらく以下のようにジェネレータを記述する:
code:rust
async fn foo(arg: Arg) -> Return yield Yield
ReturnとYieldはどちらもオプショナルで、デフォルトは()。
()を委譲するasync関数はFutureを実装し、()を返すasync関数はIteratorを実装する。
このアプローチの問題点は、Streamを上手く(ergonomically)扱えないこと:
これはPoll<Option<T>>を委譲する必要がある
async関数内のawaitが、どうやって()以外を委譲すれば上手くいくのかは不明瞭
そのため、ジェネレータ関数、async関数、asyncジェネレータ関数、にそれぞれ別の構文を用意した方が上手くいきそう
6-4. "ホットなasync関数"
「"initialization"ステップ」の節で取り上げた挙動をデフォルトにしてしまう案
最初のawait!呼び出しに達するまでは、同期的に実行してしまう
いろいろと問題がある:
実装が複雑になる (e.g., await!の扱いを最初かどうかで区別する必要がある)
futuresのモデルと乖離する:
Rustのfutureは、他の言語のそれとは異なり、勝手に処理が進行しない
Future::pollを呼び出して初めて、非同期コードが実行される
このモデルが崩れて「条件によってはpollを呼ばなくても進行する場合がある」となってしまうと混乱やバグに繋がりそう
利用者的にも複雑そう:
関数本体のある部分が即座に実行されるかどうかはawaitがどう使われるかに依存するので、それに注意する必要がある
6-5. 他の非同期システムではなくasync/awaitを使用する
他の非同期モデルもありえる:
e.g, 汎用化されたエフェクトシステム、モナド&do記法、グリーンスレッド、スタックフルコルーチン
async/awaitの上により汎用化された層を重ねることも可能かもしれないが、まだ十分な研究はなされていない
2018年の目標を見据えた場合には、async/await構文(多くの言語で利用可能な概念で、既存の非同期I/Oライブラリとも相性が良い)が最も妥当そう
6-6. asyncブロック vs asyncクロージャ
以下のように、両方で同じようなことができるので、片方だけで良いのでは?
code:rust
// almost equivalent
async { ... }
(async || { ... })()
// almost equivalent
async |..| { ... }
|..| async { ... }
確かにそうかもしれないけれど、現状は以下の理由で両方を提供している(安定化前には変わるかも):
async ||は、async fnとの一貫性のために重要
高階async関数は、サービスを構築するために便利かも
asyncブロックは、"initialization"パターンを実装したり、futureを直接生成するためのプリミティブとして便利
7. 先行技術
いろいろな言語(e.g., C#、JavaScript、Python)で、非同期操作を処理するためにasync/await構文が使われている。
現在で主流な、非同期プログラミング用のパラダイムは三つ:
asyncおよびawait記法
主に"グリーンスレッド"と呼称される、暗黙的な並行ランタイム(e.g., Go, Erlang)
遅延評価コード上のでモナド変換 (e.g., Haskell)
Rustとはasync/awaitが一番相性が良さそう:
モナドとは異なりownership/borrowingとも上手く共存可能
グリーンスレッドとは異なり、完全にライブラリベースで実現可能
他のasync/await採用の静的型付け言語(e.g., C#)と異なる点は、"内部返り値型"を採用しているところ(理由は上述の通り)。
8. 未解決の疑問
8-1. await式用の最終的な構文
このRFCではawait!を組み込みマクロとして導入したが、いつかは通常の制御フロー構造にしたい。
ただ「優先順位」や「区切り文字が必要かどうか」といったことが未解決。
特にawaitを?と組み合わせたケースが興味深い:
Resultに評価されるfutureは非常によく見かける
ユーザはそれらに対して?を使いたいはず
これはawaitが?より強い優先順位を持つべきことを示唆する
futureに対してではなく、await futureに対して?を適用したい
ただしawaitが制御フロー構造になった場合には、間にスペースが入ることになるので、以下のような直観的ではない見た目になってしまう(一見するとfutureに対して?が適用されているように見える)
code:rust
await future?
いくつかの対応策が考えられはする:
1. 何らかの区切り文字を要求する。例えば await { future }?。ただこれはノイジー。
2. 上の優先順位を明白なものと定義して、不便な優先順位を使いたい場合(訳注: futureに?を適用したい場合?)には、ユーザに(await future)?と書くことを要求する
ユーザはこの見た目に驚きそう
3. 上の優先順位を不便なものと定義する (訳注: ?)
これも他の優先順位と同様に驚きを与えそう
4. await? futureのような特殊な構文を導入する。これもとても奇妙に見える
別の解法があるのか、それとも上の中から最もマシなものを選ぶのかは、まだ未解決の疑問。
8-2. 'for await'とストリーム処理
forループで、ストリームをどう処理すべきかも、このRFCでは扱っていない問題。
IntoIteratorの代わりにIntoStreamを入力に受け取るfor awaitを考えることは出来るかもしれない:
code:rust
for await value in stream {
println!("{}", value);
}
そもそもStreamの安定化自体をまだ先送りにしたいので、このRFCでも件に関しては考えないことにする。
8-3. ジェネレータとストリーム
将来的には、futureにではなく、streamに評価されるようなasync関数を定義可能にしたくなるかもしれない。
我々は、このユースケースをジェネレータの枠組みで扱うことを提案する。
ジェネレータはイテレータの一種として評価可能で、非同期ジェネレータはストリームの一種と見做すことができる。
以下は、その例(構文はこれから変わるかも):
code:rust
// Returns an iterator of i32
fn foo(mut x: i32) yield i32 {
while x > 0 {
yield x;
x -= 2;
}
}
// Returns a stream of i32
async fn foo(io: &AsyncRead) yield i32 {
async for line in io.lines() {
yield line.unwrap().parse().unwrap();
}
}
8-4. Unpinを実装したasync関数
このRFCで提案されたように、全てのasync関数はUnpinトレイトを実装しないので、Pin構造体の外にそれを移すことはunsafe扱いとなる。
これは、async関数に、委譲地点を跨いだ参照を保持することを許可する。
さらに、アノテーションにより、タイプチェックを行い委譲地点を跨ぐ参照を一切保持しないと確信できる非同期関数に関してはUnpinトレイトの実装を許容することもできる。
これを有効にするアノテーションに関しては、当面は未指定。
8-5. asyncブロック内での?演算子と制御フロー構成要素
このRFCは、?演算子やreturn、break、continue等の制御フロー構造がasyncブロックの中でどのように動作すべきかは提案しない。
asyncブロックが?演算子の境界として振舞うべき、とうことは議論されたことがある。
これは失敗する可能性のあるI/Oを扱いやすくするかもしれない:
code:rust
let reader: AsyncRead = ...;
async {
let foo = await!(reader.read_to_end())?;
Ok(foo.parse().unwrap_or(0))
}: impl Future<Output = io::Result<u32>>
また、asyncブロックから早期脱出するためにbreakを許可すべき、ということも議論されたことがある:
code:rust
async {
if true { break "foo" }
}
returnではなくbreakを使うのは、これが周りの関数ではなく、asyncブロックに対して適用されることを示す上で有益である。
一方、クロージャとasyncクロージャの間でreturnキーワードの使い方に差異が出てしまうことにはなってしまう。