async/.await
第1章では、async/.await について簡単に見ました。
この章では、async/.await についてより詳しく説明し、
それがどのように動作するか、そして async コードが従来の Rust プログラムと
どのように異なるかを解説します。
async/.await は Rust の特別な構文要素であり、ブロックする代わりに
現在のスレッドの制御を譲ることを可能にします。これにより、ある操作の完了を
待っている間に、他のコードを進めることができます。
async を使用する主な方法は 2 つあります。async fn と async ブロックです。
どちらも Future トレイトを実装する値を返します。
// `foo()` returns a type that implements `Future<Output = u8>`.
// `foo().await` will result in a value of type `u8`.
async fn foo() -> u8 { 5 }
fn bar() -> impl Future<Output = u8> {
// This `async` block results in a type that implements
// `Future<Output = u8>`.
async {
let x: u8 = foo().await;
x + 5
}
}
第1章で見たように、async 本体やその他の future は遅延評価されます。
つまり、実行されるまで何もしません。Future を実行する最も一般的な方法は、
それに対して .await することです。Future に対して .await が呼び出されると、
それを完了まで実行しようとします。Future がブロックされている場合、
現在のスレッドの制御を譲ります。さらに進められるようになると、Future は
executor によって取り上げられて実行を再開し、.await が解決できるようになります。
async のライフタイム
従来の関数とは異なり、参照やその他の非 static 引数を取る async fn は、
引数のライフタイムによって制限される Future を返します。
// This function:
async fn foo(x: &u8) -> u8 { *x }
// Is equivalent to this function:
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
async move { *x }
}
これは、async fn から返される future は、その非 static 引数がまだ有効な間に
.await されなければならないことを意味します。関数を呼び出した直後に
future を .await する一般的なケース(foo(&x).await のような場合)では、
これは問題になりません。しかし、future を保存したり、別のタスクやスレッドに
送ったりする場合には、問題になる可能性があります。
参照を引数として持つ async fn を static future に変換するための一般的な
回避策の 1 つは、async fn の呼び出しと引数を async ブロック内にまとめることです。
fn bad() -> impl Future<Output = u8> {
let x = 5;
borrow_x(&x) // ERROR: `x` does not live long enough
}
fn good() -> impl Future<Output = u8> {
async {
let x = 5;
borrow_x(&x).await
}
}
引数を async ブロックに移動することで、そのライフタイムを good の呼び出しから
返される Future のライフタイムと一致するように延長します。
async move
async ブロックとクロージャでは、通常のクロージャと同様に move キーワードを
使用できます。async move ブロックは、参照する変数の所有権を取得します。
これにより、現在のスコープより長く生存できるようになりますが、それらの変数を
他のコードと共有する能力は失われます。
/// `async` block:
///
/// Multiple different `async` blocks can access the same local variable
/// so long as they're executed within the variable's scope
async fn blocks() {
let my_string = "foo".to_string();
let future_one = async {
// ...
println!("{my_string}");
};
let future_two = async {
// ...
println!("{my_string}");
};
// Run both futures to completion, printing "foo" twice:
let ((), ()) = futures::join!(future_one, future_two);
}
/// `async move` block:
///
/// Only one `async move` block can access the same captured variable, since
/// captures are moved into the `Future` generated by the `async move` block.
/// However, this allows the `Future` to outlive the original scope of the
/// variable:
fn move_block() -> impl Future<Output = ()> {
let my_string = "foo".to_string();
async move {
// ...
println!("{my_string}");
}
}
マルチスレッド Executor における .await
マルチスレッドの Future executor を使用する場合、Future はスレッド間を
移動する可能性があることに注意してください。そのため、async 本体で使用される
すべての変数は、スレッド間を移動できなければなりません。どの .await も、
新しいスレッドへの切り替えを引き起こす可能性があるためです。
これは、Send トレイトを実装していない Rc、&RefCell、その他の型を使用することは
安全ではないことを意味します。これには、Sync トレイトを実装していない型への参照も
含まれます。
(注意: .await の呼び出し中にこれらの型がスコープ内にない限り、これらの型を
使用することは可能です。)
同様に、従来の future を意識しないロックを .await をまたいで保持することは
よい考えではありません。これはスレッドプールをロックアップさせる可能性があります。
あるタスクがロックを取得し、.await して executor に制御を譲ると、別のタスクが
そのロックを取得しようとしてデッドロックを引き起こす可能性があります。これを避けるには、
std::sync のものではなく、futures::lock の Mutex を使用してください。