Rust非同期プログラミングasync/.await原理解析(一)


このチュートリアルではrust非同期コードasync/を詳細に分析します.awaitの内部動作メカニズム.tokioではなくasync-stdライブラリを使用します.async/をサポートするのは初めてです.await構文のrustライブラリ.async/.await原理解析チュートリアルは2つの部分に分かれています.これは第1部です.
ブロックチェーン開発チュートリアルリンク:以太坊|ビットコイン|EOS|Tendermint|Hyperledger Fabric|Omni/USDT|Ripple
0、Rust練習環境の準備
まず、Cargoプロジェクトを作成します.
~$ cargo new --bin sleepus-interruptus

チュートリアルで使用するコンパイラと一致する場合は、1.39.0のrust-toolchainファイルを追加できます.
次の内容を続行する前に、cargo runを実行して環境に問題がないことを確認します.
1、交互に表示するRustプログラム
毎回0.5秒間隔で10回のSleepusメッセージを表示できる簡単なプログラムを書きます.Interruptusメッセージを同時に5回表示し、毎回1秒間隔で表示します.次はかなり簡単なrust実装コードです.
use std::thread::{sleep};
use std::time::Duration;

fn sleepus() {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500));
    }
}

fn interruptus() {
    for i in 1..=5 {
        println!("Interruptus {}", i);
        sleep(Duration::from_millis(1000));
    }
}

fn main() {
    sleepus();
    interruptus();
}

ただし、上記のコードは同期して2つの操作を実行し、すべてのSleepusメッセージを表示してからInterruptusメッセージを表示します.この2つのメッセージがインターリーブ表示されることが望ましい.すなわち、InterruptusメッセージはSleepusメッセージの表示を中断することができる.
インターリーブ表示のターゲットを実現するには、2つの方法があります.明らかなのは、各関数に個別のスレッドを作成し、スレッドの実行が完了するのを待つことです.
use std::thread::{sleep, spawn};

fn main() {
    let sleepus = spawn(sleepus);
    let interruptus = spawn(interruptus);

    sleepus.join().unwrap();
    interruptus.join().unwrap();
}

次の点に注意してください.
  • spawn(sleepus)ではなくspawn(sleepus())を使用してスレッドを作成します.後者は直ちにsleepus()を実行し、その実行結果をspawnに伝える.これは私たちが望んでいるものではない.私は主関数種でjoin()を使用してサブスレッドの終了を待ち、unwrap()を使用して発生可能な障害を処理する.私は怠け者だからだ.

  • もう1つの実装方法は、補助スレッドを作成し、メインスレッドの関数の1つを呼び出すことです.
    fn main() {
        let sleepus = spawn(sleepus);
        interruptus();
    
        sleepus.join().unwrap();
    }
    

    この方法は,スレッドを追加的に作成するだけで副作用もないため,より効率的であるため,この方法を推奨する.
    しかし、この2つの方法は非同期ソリューションではありません.オペレーティングシステムが管理する2つのスレッドを使用して、2つの同期タスクを同時に実行します.次に、単一スレッド内で2つのタスクをコラボレーションして実行する方法を試してみましょう.
    2、Rust非同期async/.await交互表示プログラムの実装
    より高い階層の抽象から始め,rust非同期プログラミングの詳細に徐々に深く入り込む.では、asyncスタイルで前のアプリケーションを書き直しましょう.
    まずカーゴでtomlに次の依存度を追加します.
    async-std = { version = "1.2.0", features = ["attributes"] }
    

    アプリケーションを次のように書き換えることができます.
    use async_std::task::{sleep, spawn};
    use std::time::Duration;
    
    async fn sleepus() {
        for i in 1..=10 {
            println!("Sleepus {}", i);
            sleep(Duration::from_millis(500)).await;
        }
    }
    
    async fn interruptus() {
        for i in 1..=5 {
            println!("Interruptus {}", i);
            sleep(Duration::from_millis(1000)).await;
        }
    }
    
    #[async_std::main]
    async fn main() {
        let sleepus = spawn(sleepus());
        interruptus().await;
    
        sleepus.await;
    }
    

    主な修正の説明は以下の通りです.
  • std::threadのsleep関数とspawn関数ではなくasync_を使用します.std::task.- sleepus関数とinterruptus関数の両方にasync
  • を追加
  • sleepを呼び出した後、.awaitを追加しました.注意.await()呼び出しではなく、新しい構文
  • です.
  • 主関数に#[async_std::main]属性
  • を用いる.
  • 主関数の前にasyncキーワード
  • もあります
  • spawn(sleepus())ではなくspawn(sleepus)を使用しています.これはsleepusを直接呼び出しspawn
  • に結果を伝えることを意味します.
  • interruptus()の呼び出しが増加する.await
  • はsleepusに対してjoin()を使用するのではなく、改用する.await構文
  • 多くの修正があるように見えますが、実際には、私たちのコード構造は以前のバージョンとほぼ一致しています.プログラムが実行されるのは、単一のスレッドを使用してブロックなし呼び出しを行うことです.
    次に、上記の修正が何を意味するのかを分析します.
    3、asyncキーワードの役割
    関数定義の前にasyncを追加するには、主に次の3つのことをしました.
  • これにより、関数内で使用できます.await構文.これからこの点について深く検討します
  • 関数の戻りタイプを変更します.async fn foo()->Barは実際にimpl std::future::Future
  • を返します.
  • 結果値を新しいFutureオブジェクトに自動的にカプセル化します.
  • について詳しく説明します
    2点目について説明しましょう.Rustの標準ライブラリにはFutureというtraitがあり、Futureには関連タイプOutputがあります.このtraitの意味は、私が任務を完成したとき、Outputのタイプの値をあげることを約束します.例えば、非同期HTTPクライアントがこのように実現する可能性があります.
    impl HttpRequest {
        fn perform(self) -> impl Future { ... }
    }
    

    HTTPリクエストを送信するには、ブロックされていないI/Oが必要です.呼び出しスレッドをブロックすることは望んでいませんが、最終的に応答結果を得る必要があります.async fn sleepus()の結果タイプには、()が隠されています.そのため、私たちのFutureのOutputも()であるべきです.これは、次のように関数を変更する必要があることを意味します.
    fn sleepus() -> impl std::future::Future
    

    ただし、ここだけを変更すると、コンパイルで次のエラーが発生します.
    error[E0728]: `await` is only allowed inside `async` functions and blocks
     --> src/main.rs:7:9
      |
    4 | fn sleepus() -> impl std::future::Future {
      |    ------- this is not `async`
    ...
    7 |         sleep(Duration::from_millis(500)).await;
      |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only allowed inside `async` functions and blocks
    
    error[E0277]: the trait bound `(): std::future::Future` is not satisfied
     --> src/main.rs:4:17
      |
    4 | fn sleepus() -> impl std::future::Future {
      |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::future::Future` is not implemented for `()`
      |
      = note: the return type of a function must have a statically known size
    

    最初のエラー情報は直接的です.async関数またはコードブロックでしか使用できません.await構文.非同期コードブロックにはまだ接触していませんが、このように見えます.
    async {
        // async noises intensify
    }
    

    2番目のエラーメッセージは、asyncキーワード要求関数の戻りタイプがimpl Futureである最初の結果です.このキーワードがなければ、私たちのloop結果タイプは()で、これは明らかに要求を満たしていません.
    関数体全体を非同期コードブロックで包むと問題が解決します.
    fn sleepus() -> impl std::future::Future {
        async {
            for i in 1..=10 {
                println!("Sleepus {}", i);
                sleep(Duration::from_millis(500)).await;
            }
        }
    }
    

    4、.await構文の役割
    これらのasync/をすべて必要としないかもしれません.await.sleepusを除去するとawaitはどうなるの?驚いたことに、コンパイルが通過しました.警告がありましたが.
    warning: unused implementer of `std::future::Future` that must be used
     --> src/main.rs:8:13
      |
    8 |             sleep(Duration::from_millis(500));
      |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      |
      = note: `#[warn(unused_must_use)]` on by default
      = note: futures do nothing unless you `.await` or poll them
    

    Future値を生成していますが、使用していません.プログラムの出力を表示すると、コンパイラの警告がどういう意味か理解できます.
    Interruptus 1
    Sleepus 1
    Sleepus 2
    Sleepus 3
    Sleepus 4
    Sleepus 5
    Sleepus 6
    Sleepus 7
    Sleepus 8
    Sleepus 9
    Sleepus 10
    Interruptus 2
    Interruptus 3
    Interruptus 4
    Interruptus 5
    

    私たちのすべてのSleepusメッセージの出力に遅延はありません.問題はsleepの呼び出しが実際に現在のスレッドを休ませなかったことであり,Futureを実現した値を生成しただけであり,最終的な実現を約束したとき,確かに遅延が発生したことを知っている.しかし,我々はFutureを単純に無視したため,実際には遅延を利用しなかった.
    理解するためにawait構文はいったい何をしたのか,次にFuture値を直接用いて関数を実現する.まずasyncブロックを使わないことから始めます.
    5、asyncキーワードを使用しないRust非同期コード
    asyncコードブロックを失うと、次のように見えます.
    fn sleepus() -> impl std::future::Future {
        for i in 1..=10 {
            println!("Sleepus {}", i);
            sleep(Duration::from_millis(500));
        }
    }
    

    コンパイル中に次のエラーが発生します.
    error[E0277]: the trait bound `(): std::future::Future` is not satisfied
     --> src/main.rs:4:17
      |
    4 | fn sleepus() -> impl std::future::Future {
      |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::future::Future` is not implemented for `()`
      |
    

    上記のエラーは、forループの結果タイプが()であり、Futureというtraitは実現されていないためである.この問題を修復する1つの方法は,forループの後にFutureの実装タイプに戻るように一言加えることである.これを使うことができることを知っています:sleep:
    fn sleepus() -> impl std::future::Future {
        for i in 1..=10 {
            println!("Sleepus {}", i);
            sleep(Duration::from_millis(500));
        }
        sleep(Duration::from_millis(0))
    }
    

    forループメモリに未使用のFuture値があるという警告メッセージが表示されますが、戻り値のエラーは解決されました.このsleep呼び出しは実際には何もしていません.本当の占有率Futureに置き換えることができます.
    fn sleepus() -> impl std::future::Future {
        for i in 1..=10 {
            println!("Sleepus {}", i);
            sleep(Duration::from_millis(500));
        }
        async_std::future::ready(())
    }
    

    6、自分のFutureを実現する
    土鍋を破って最後まで聞くために、asyncは適用されません.stdライブラリのready関数ではなく,独自の実装Futureの構造を定義する.DoNothingと呼びましょう.
    use std::future::Future;
    
    struct DoNothing;
    
    fn sleepus() -> impl Future {
        for i in 1..=10 {
            println!("Sleepus {}", i);
            sleep(Duration::from_millis(500));
        }
        DoNothing
    }
    

    問題はDoNothingがまだFuture実装を提供していないことです.次に、いくつかのコンパイラドライバの開発を行い、rustcにこのプログラムを修復する方法を教えてもらいます.最初のエラーメッセージは次のとおりです.
    the trait bound `DoNothing: std::future::Future` is not satisfied
    

    このtraitの実装を補完しましょう
    impl Future for DoNothing {
    }
    

    エラーの継続:
    error[E0046]: not all trait items implemented, missing: `Output`, `poll`
     --> src/main.rs:7:1
      |
    7 | impl Future for DoNothing {
      | ^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Output`, `poll` in implementation
      |
      = note: `Output` from trait: `type Output;`
      = note: `poll` from trait: `fn(std::pin::Pin, &mut std::task::Context) -> std::task::Poll<::Output>`
    
    PinまたはContextを本当に知っているわけではありませんが、Outputを知っています.私たちは前に()に戻ったので、今はそのようにしましょう.
    use std::pin::Pin;
    use std::task::{Context, Poll};
    
    impl Future for DoNothing {
        type Output = ();
    
        fn poll(self: Pin, ctx: &mut Context) -> Poll<:output> {
            unimplemented!()
        }
    }
    

    ああ!コンパイル合格!もちろん、unimplemented!()呼び出しのため、実行中に失敗します.
    thread 'async-std/executor' panicked at 'not yet implemented', src/main.rs:13:9
    

    ではpollを実現してみましょう.Poll<:output/>またはPollのタイプの値を返す必要があります.Pollの定義を見てみましょう.
    pub enum Poll {
        Ready(T),
        Pending,
    }
    

    いくつかの基本的な推理を利用して、Readyは「私たちのFutureはすでに完成しており、これは出力です」と述べ、Pendingは「まだ終わっていません」と述べたことを理解することができます.DoNothingが()型の出力をすぐに返すことを望んでいるとします.
    fn poll(self: Pin, _ctx: &mut Context) -> Poll<:output> {
        Poll::Ready(())
    }
    

    おめでとう!あなたは自分の最初のFuture構造を実現したばかりです!
    7、asyncと関数の戻り値
    asyncが関数に対して3つ目のことをしたことを覚えていますか.結果値を自動的に新しいFutureにカプセル化します.私たちは次にこの点を示します.
    まずsleepusの定義を簡略化します.
    fn sleepus() -> impl Future {
        DoNothing
    }
    

    コンパイルと正常に動作.asyncスタイルに戻ります.
    async fn sleepus() {
        DoNothing
    }
    

    この場合、エラーが発生します.
    error[E0271]: type mismatch resolving `::Output == ()`
      --> src/main.rs:17:20
       |
    17 | async fn sleepus() {
       |                    ^ expected struct `DoNothing`, found ()
       |
       = note: expected type `DoNothing`
                  found type `()`
    

    async関数やコードブロックがあると、結果はFuture実装オブジェクトに自動的にカプセル化されます.そのため、impl Futureを返す必要があります.現在、私たちのタイプはOutput=()です.
    処理は簡単で、DoNothingの後に簡単に追加するだけです.await:
    async fn sleepus() {
        DoNothing.await
    }
    

    これは私たちに正しいです.awaitの役割は、DoNothingからOutput値を抽出する直感を増やした.しかし、私たちは依然としてそれがどのように実現されているかを本当に理解していません.では、より複雑なFutureを実現して探索を続けましょう.
    原文リンク:Rust非同期プログラミングasync/.await原理解析(一)—集智網