ハイブリッドタイプのものですか?(たとえば、私の.NETプログラムは、非同期呼び出しにヒットするまでスタックを使用しますが、完了するまで他の構造に切り替えます。その時点で、スタックは次の項目を確認できる状態に戻されますか? )
基本的にはい。
私たちが持っていると仮定します
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
これは、継続が具体化される方法の非常に簡略化された説明です。実際のコードはかなり複雑ですが、これはアイデアを全体に広めます。
ボタンをクリックします。メッセージがキューに入っています。メッセージループはメッセージを処理し、クリックハンドラーを呼び出して、メッセージキューの戻りアドレスをスタックに配置します。つまり、ハンドラーが完了した後に発生するのは、メッセージループが実行を継続する必要があるということです。したがって、ハンドラの続きはループです。
クリックハンドラーはFoo()を呼び出し、自身の戻りアドレスをスタックに格納します。つまり、Fooの続きはクリックハンドラーの残りの部分です。
FooはTask.Delayを呼び出し、自分自身の戻りアドレスをスタックに入れます。
Task.Delayは、すぐにタスクを返すために必要な魔法を実行します。スタックがポップされ、Fooに戻ります。
Fooは返されたタスクをチェックして、完了したかどうかを確認します。そうではない。待機の継続はBlah()を呼び出すことなので、FooはBlah()を呼び出すデリゲートを作成し、タスクの継続としてデリゲートする署名をします。(私はちょっと誤った説明をしましたが、あなたはそれをキャッチしましたか?もしそうでなければ、すぐにそれを明らかにします。)
次に、Fooは独自のTaskオブジェクトを作成し、それを未完成としてマークし、スタックの上にあるクリックハンドラーに返します。
クリックハンドラーはFooのタスクを調べ、それが不完全であることを発見します。ハンドラーでのawaitの継続はBar()の呼び出しであるため、クリックハンドラーは、Bar()を呼び出すデリゲートを作成し、Foo()によって返されるタスクの継続として設定します。次に、スタックをメッセージループに戻します。
メッセージループはメッセージを処理し続けます。最終的に、遅延タスクによって作成されたタイマーマジックが機能し、遅延タスクの継続を実行できるというメッセージをキューに投稿します。そのため、メッセージループはタスクの継続を呼び出し、通常どおりスタックにスタックします。そのデリゲートはBlah()を呼び出します。Blah()はそれを実行し、スタックを返します。
今、何が起こりますか?ここでトリッキーなビットです。遅延タスクの継続は、Blah()を呼び出すだけではありません。 また、Bar()の呼び出しをトリガーする必要がありますが、そのタスクはBarについて知りません!
Foo は、(1)Blah()を呼び出すデリゲートを実際に作成し、(2)Fooが作成し、イベントハンドラーに返すタスクの継続を呼び出します。これが、Bar()を呼び出すデリゲートを呼び出す方法です。
これで、必要なすべてのことを正しい順序で実行できました。ただし、メッセージループ内のメッセージの処理を長時間停止することはなかったため、アプリケーションは応答性を維持しました。
これらのシナリオはスタックに対して高度すぎると言うことは完全に理にかなっていますが、スタックを置き換えるものは何ですか?
デリゲートのクロージャクラスを介した相互参照を含むタスクオブジェクトのグラフ。これらのクロージャクラスは、最後に実行されたawaitの位置とローカルの値を追跡する状態マシンです。さらに、この例では、オペレーティングシステムによって実装されたアクションのグローバル状態キューと、それらのアクションを実行するメッセージループ。
演習:メッセージループのない世界でこれがすべてうまくいくとどう思いますか?たとえば、コンソールアプリケーション。コンソールアプリで待つのはかなり異なります。これまでに知っていることから、それがどのように機能するかを推測できますか?
私がこの数年前に知ったとき、スタックは非常に高速で軽量であり、アプリケーションからヒープから離れて割り当てられたメモリの一部であり、手元のタスクの効率的な管理をサポートしていたので(スタックは意図されていましたか?)。何が変わったの?
スタックは、メソッドのアクティブ化の有効期間がスタックを形成する場合に便利なデータ構造ですが、私の例では、クリックハンドラーのアクティブ化Foo、Bar、Blahはスタックを形成しません。したがって、そのワークフローを表すデータ構造をスタックにすることはできません。むしろ、ワークフローを表す、ヒープに割り当てられたタスクとデリゲートのグラフです。待機とは、ワークフローの中で、先に開始された作業が完了するまでワークフローをさらに進めることができないポイントです。待機中に、特定の開始済みタスクが完了したかどうかに依存しない他の作業を実行できます。
スタックは単なるフレームの配列であり、フレームには、(1)関数の中央(呼び出しが発生した場所)へのポインター、および(2)ローカル変数と一時の値が含まれます。タスクの継続は同じです。デリゲートは関数へのポインタであり、関数の途中(待機が発生した場所)の特定のポイントを参照する状態にあり、クロージャには各ローカル変数または一時変数のフィールドがあります。フレームはもうすてきなきちんとした配列を形成しませんが、すべての情報は同じです。