web-dev-qa-db-ja.com

Rustのasync / awaitの目的は何ですか?

C#のような言語では、次のコードを与えます(意図的にawaitキーワードを使用していません):

async Task Foo()
{
    var task = LongRunningOperationAsync();

    // Some other non-related operation
    AnotherOperation();

    result = task.Result;
}

最初の行では、長い操作が別のスレッドで実行され、Taskが返されます(これは未来です)。その後、最初の操作と並行して実行される別の操作を実行し、最後に操作が完了するのを待つことができます。 Python、JavaScriptなどのasync/awaitの動作でもあると思います。

一方、Rustでは、 RFC と読みました:

Rustの先物と他の​​言語の先物との根本的な違いは、Rustの先物はポーリングされない限り何もしないということです。システム全体がこれを中心に構築されています。たとえば、キャンセルはまさにこの理由で未来を落としています。対照的に、他の言語では、非同期fnを呼び出すと、すぐに実行を開始するFutureがスピンアップします。

この場合、Rustのasync/awaitの目的は何ですか?他の言語を見ると、この表記は並列操作を実行する便利な方法ですが、async関数の呼び出しが何も実行しない場合、Rustでどのように動作するかわかりません。

12

いくつかの概念を統合しています。

同時実行性は並列処理ではありません 、およびasyncawaitconcurrencyのツールです。並列処理のためのツール。

さらに、未来がすぐにポーリングされるかどうかは、選択された構文に直交します。

async/await

キーワードasyncおよびawaitは、非同期コードの作成と対話をより読みやすく、「通常の」同期コードのように見せるために存在します。これは、私が知っている限り、そのようなキーワードを持つすべての言語に当てはまります。

よりシンプルなコード

これは、ポーリング時に2つの数値を追加するFutureを作成するコードです

before

fn long_running_operation(a: u8, b: u8) -> impl Future<Output = u8> {
    struct Value(u8, u8);

    impl Future for Value {
        type Output = u8;

        fn poll(self: Pin<&mut Self>, _lw: &LocalWaker) -> Poll<Self::Output> {
            Poll::Ready(self.0 + self.1)
        }
    }

    Value(a, b)
}

after

async fn long_running_operation(a: u8, b: u8) -> u8 {
    a + b
}

「前の」コードは基本的に 今日のpoll_fn関数の実装 であることに注意してください

ピーターホールの答え も参照してください。多くの変数を追跡する方法を改善する方法について。

参照資料

async/awaitに関して驚く可能性のあることの1つは、これまで不可能だった特定のパターンを有効にすることです。先物で参照を使用します。バッファーを非同期的に値で満たすコードを次に示します。

before

use std::io;

fn fill_up<'a>(buf: &'a mut [u8]) -> impl Future<Output = io::Result<usize>> + 'a {
    futures::future::lazy(move |_| {
        for b in buf.iter_mut() { *b = 42 }
        Ok(buf.len())
    })
}

fn foo() -> impl Future<Output = Vec<u8>> {
    let mut data = vec![0; 8];
    fill_up(&mut data).map(|_| data)
}

これはコンパイルに失敗します:

error[E0597]: `data` does not live long enough
  --> src/main.rs:33:17
   |
33 |     fill_up_old(&mut data).map(|_| data)
   |                 ^^^^^^^^^ borrowed value does not live long enough
34 | }
   | - `data` dropped here while still borrowed
   |
   = note: borrowed value must be valid for the static lifetime...

error[E0505]: cannot move out of `data` because it is borrowed
  --> src/main.rs:33:32
   |
33 |     fill_up_old(&mut data).map(|_| data)
   |                 ---------      ^^^ ---- move occurs due to use in closure
   |                 |              |
   |                 |              move out of `data` occurs here
   |                 borrow of `data` occurs here
   |
   = note: borrowed value must be valid for the static lifetime...

after

use std::io;

async fn fill_up(buf: &mut [u8]) -> io::Result<usize> {
    for b in buf.iter_mut() { *b = 42 }
    Ok(buf.len())
}

async fn foo() -> Vec<u8> {
    let mut data = vec![0; 8];
    fill_up(&mut data).await.expect("IO failed");
    data
}

これは動作します!

async関数を呼び出しても何も実行されません

一方、Futureおよびfutureを取り巻くシステム全体の実装と設計は、キーワードasyncおよびawaitとは無関係です。確かに、Rust=は、Tokioのように)async/awaitキーワードが存在する前に、活発な非同期エコシステムを持っています。同じことがJavaScriptにも当てはまりました。

作成時にFuturesがすぐにポーリングされないのはなぜですか?

最も信頼できる回答については、RFCプルリクエストで withoutboatsからのこのコメント を確認してください。

Rustの先物と他の​​言語の先物との根本的な違いは、Rustの先物はポーリングされない限り何もしないということです。システム全体がこれを中心に構築されています。たとえば、キャンセルはまさにこの理由で未来を落としています。対照的に、他の言語では、非同期fnを呼び出すと、すぐに実行を開始するFutureがスピンアップします。

これに関するポイントは、async&await in Rustは本質的にコンカレント構造ではありません。async&awaitのみを使用し、同時実行プリミティブを使用しないプログラムがある場合、プログラム内のコードは定義された静的に知られた線形順序。明らかに、ほとんどのプログラムは、イベントループで複数の同時タスクをスケジュールするために何らかの並行性を使用しますが、そうする必要はありません。特定のイベントの順序。非ブロッキングIOが実行され、非ローカルイベントの大きなセットと非同期にしたい場合(例:内のイベントの順序を厳密に制御できます)リクエストハンドラー、他の多くのリクエストハンドラーと同時に、待機ポイントの両側であっても)。

このプロパティは、Rustのasync/await構文に、Rustが何であるかを示すローカルな推論と低レベルの制御を提供します。コードがいつ実行されたかはまだわかっていますが、待機の前後に応じて2つの異なる場所で実行されますが、すぐに実行を開始するという他の言語の決定は、タスクをすぐにスケジュールするシステムに起因すると思います非同期fnを呼び出すときに同時に(たとえば、Dart 2.0ドキュメントから得た根本的な問題の印象です)。

Dart 2.0の背景の一部は munificentからのこの議論 でカバーされています:

こんにちは、私はDartチームにいます。 Dartのasync/awaitは、主にC#のasync/awaitにも取り組んでいるErik Meijerによって設計されました。 C#では、async/awaitは最初のawaitと同期しています。 Dartの場合、Erikと他の人は、C#のモデルがあまりにも混乱していると感じ、代わりにコードを実行する前に非同期関数が常に1回生成することを指定しました。

当時、私と私のチームの別のメンバーは、パッケージマネージャーで進行中の新しい構文とセマンティクスを試すために、モルモットになるという任務を負っていました。その経験に基づいて、非同期関数は最初の待機と同期して実行する必要があると感じました。私たちの議論はほとんどでした:

  1. 一度降伏すると、正当な理由もなくパフォーマンスが低下します。ほとんどの場合、これは重要ではありませんが、実際には問題になる場合があります。あなたがそれと一緒に暮らすことができる場合でさえ、それはどこでも小さなパフォーマンスを出血させるドラッグです。

  2. 常に譲歩するということは、async/awaitを使用して特定のパターンを実装できないことを意味します。特に、次のようなコードを使用するのが一般的です(ここでは擬似コード)。

    getThingFromNetwork():
      if (downloadAlreadyInProgress):
        return cachedFuture
    
      cachedFuture = startDownload()
      return cachedFuture
    

    つまり、完了する前に複数回呼び出すことができる非同期操作があります。後の呼び出しでは、以前に作成された保留中の将来と同じものが使用されます。操作を複数回開始しないようにします。つまり、操作を開始する前にキャッシュを同期的に確認する必要があります。

    非同期関数が最初から非同期である場合、上記の関数はasync/awaitを使用できません。

私たちは私たちの主張を認めましたが、最終的には言語設計者はトップから非同期に固執しました。これは数年前です。

それは間違った呼び出しであることが判明しました。パフォーマンスコストは実に多く、多くのユーザーが「非同期機能は遅い」という考え方を開発し、パフォーマンスヒットが手頃な価格であったとしても、その使用を避け始めました。さらに悪いことに、関数の最上部でいくつかの同期作業を実行できると考え、競合状態を作成したことに気付いてがっかりする、厄介な同時実行バグがあります。全体として、ユーザーはコードを実行する前に非同期関数が生成されると自然に想定していないようです。

そのため、Dart 2の場合、非同期関数を最初の待機と同期するように変更するための非常に苦痛な破壊的変更を行い、その移行を通じて既存のコードをすべて移行しています。変更を行っていることを嬉しく思いますが、初日には正しいことをしたいと思います。

Rustの所有権とパフォーマンスモデルが、トップからの非同期が本当に優れている場合に異なる制約を課すかどうかはわかりませんが、私たちの経験から、sync-to-the-first-waitがDartのより良いトレードオフです。

cramert replies (この構文の一部は現在古くなっていることに注意してください):

将来がポーリングされるときではなく、関数が呼び出されたときにすぐに実行するコードが必要な場合は、次のように関数を記述できます。

fn foo() -> impl Future<Item=Thing> {
    println!("prints immediately");
    async_block! {
        println!("prints when the future is first polled");
        await!(bar());
        await!(baz())
    }
}

コード例

これらの例では、1.37.0-nightly(2019-06-05)の非同期サポートとfutures-preview crate(0.3.0-alpha.16)を使用しています。

C#コードのリテラル転写

#![feature(async_await)]

async fn long_running_operation(a: u8, b: u8) -> u8 {
    println!("long_running_operation");

    a + b
}

fn another_operation(c: u8, d: u8) -> u8 {
    println!("another_operation");

    c * d
}

async fn foo() -> u8 {
    println!("foo");

    let sum = long_running_operation(1, 2);

    another_operation(3, 4);

    sum.await
}

fn main() {
    let task = foo();

    futures::executor::block_on(async {
        let v = task.await;
        println!("Result: {}", v);
    });
}

fooを呼び出した場合、Rustのイベントのシーケンスは次のようになります。

  1. Future<Output = u8>を実装するものが返されます。

それでおしまい。 「実際の」作業はまだ行われていません。 fooの結果を取得し、(この場合はfutures::executor::block_onを介してポーリングすることにより)完了に向かって進める場合、次の手順は次のとおりです。

  1. Future<Output = u8>を実装するものがlong_running_operationの呼び出しから返されます(まだ動作を開始していません)。

  2. another_operationは同期的であるため機能します。

  3. .await構文により、long_running_operationのコードが開始されます。 foo futureは、計算が完了するまで "not ready"を返し続けます。

出力は次のようになります。

foo
another_operation
long_running_operation
Result: 3

ここにはスレッドプールがないことに注意してください。これはすべて単一のスレッドで行われます。

asyncブロック

asyncブロックを使用することもできます:

use futures::{future, FutureExt};

fn long_running_operation(a: u8, b: u8) -> u8 {
    println!("long_running_operation");

    a + b
}

fn another_operation(c: u8, d: u8) -> u8 {
    println!("another_operation");

    c * d
}

async fn foo() -> u8 {
    println!("foo");

    let sum = async { long_running_operation(1, 2) };
    let oth = async { another_operation(3, 4) };

    let both = future::join(sum, oth).map(|(sum, _)| sum);

    both.await
}

ここでは、同期コードをasyncブロックでラップし、この関数が完了する前に両方のアクションが完了するのを待ちます。

このような同期コードのラッピングは、ではなく、実際に長時間かかるものには適していません。詳細については、 future-rsでブロッキングI/Oをカプセル化する最良の方法は何ですか? を参照してください。

スレッドプール付き

use futures::{executor::ThreadPool, future, task::SpawnExt, FutureExt};

async fn foo(pool: &mut ThreadPool) -> u8 {
    println!("foo");

    let sum = pool
        .spawn_with_handle(async { long_running_operation(1, 2) })
        .unwrap();
    let oth = pool
        .spawn_with_handle(async { another_operation(3, 4) })
        .unwrap();

    let both = future::join(sum, oth).map(|(sum, _)| sum);

    both.await
}
23
Shepmaster

いくつかのデータを取得し、処理し、前の手順に基づいてさらにデータを取得し、要約し、結果を出力するこの単純な擬似JavaScriptコードを考えます。

getData(url)
   .then(response -> parseObjects(response.data))
   .then(data -> findAll(data, 'foo'))
   .then(foos -> getWikipediaPagesFor(foos))
   .then(sumPages)
   .then(sum -> console.log("sum is: ", sum));

async/awaitフォーム、それは:

async {
    let response = await getData(url);
    let objects = parseObjects(response.data);
    let foos = findAll(objects, 'foo');
    let pages = await getWikipediaPagesFor(foos);
    let sum = sumPages(pages);
    console.log("sum is: ", sum);
}

それは多くの使い捨ての変数を導入し、約束のある元のバージョンよりも間違いなく悪いです。なぜわざわざ?

計算の後半で変数responseobjectsが必要になるこの変更を検討してください。

async {
    let response = await getData(url);
    let objects = parseObjects(response.data);
    let foos = findAll(objects, 'foo');
    let pages = await getWikipediaPagesFor(foos);
    let sum = sumPages(pages, objects.length);
    console.log("sum is: ", sum, " and status was: ", response.status);
}

そして、約束を付けて元の形に書き直してみてください:

getData(url)
   .then(response -> Promise.resolve(parseObjects(response.data))
       .then(objects -> Promise.resolve(findAll(objects, 'foo'))
           .then(foos -> getWikipediaPagesFor(foos))
           .then(pages -> sumPages(pages, objects.length)))
       .then(sum -> console.log("sum is: ", sum, " and status was: ", response.status)));

前の結果を参照する必要があるたびに、構造全体を1レベル深くネストする必要があります。これはすぐに読み取りと保守が非常に難しくなる可能性がありますが、async/awaitバージョンはこの問題の影響を受けません。

6
Peter Hall

async/await in Rustの目的は、同時実行のためのツールキットを提供することです。C#や他の言語と同じです。

C#とJavaScriptでは、asyncメソッドはすぐに実行を開始し、awaitの結果に関係なくスケジュールされます。 Python and Rust、asyncメソッドを呼び出しても、awaitになるまで何も起こりません(スケジュールされていません)。どちらの方法でも同じプログラミングスタイル。

別のタスク(現在のタスクと並行して実行され、現在のタスクとは独立して実行される)を生成する機能が欠けていることは正しいと思います。追加されるかもしれません。 (Rustのasyncはまだ完了していないことに注意してください。設計はまだ進化しています。)


whyRust asyncはC#とまったく同じではありませんが、 2つの言語:

  • Rustはグローバルな可変状態を阻止します。 C#およびJSでは、すべてのasyncメソッド呼び出しがグローバル可変キューに暗黙的に追加されます。暗黙的なコンテキストに対する副作用です。良くも悪くも、それはRustのスタイルではありません。

  • Rustはフレームワークではありません。 C#がデフォルトのイベントループを提供することは理にかなっています。また、優れたガベージコレクターも提供します。他の言語で標準になっているものの多くは、Rustのオプションのライブラリです。

3
Jason Orendorff