web-dev-qa-db-ja.com

非同期ボイド、ASP.Net、および未処理操作の数

ASP.Netアプリケーションの非同期voidメソッドが次の例外を引き起こす理由を理解しようとしていますが、非同期タスクはそうではないようです:

System.InvalidOperationException: An asynchronous module or handler 
completed while an asynchronous operation was still pending

私は.NETの非同期の世界に比較的新しいですが、次のすべてを含む多くの既存のリソースを介してこれを実行しようとしたように感じます。

これらのリソースから、通常はTaskを返し、非同期のvoidを回避することがベストプラクティスであることを理解しています。また、メソッドが呼び出されるとasync voidは未処理の操作の数を増やし、完了したら減ることを理解しています。これは、少なくとも私の質問に対する答えの一部のように聞こえます。しかし、私が行方不明になっているのは、Taskを返したときに何が起こるか、そしてそうすることで物事が「機能」するようになる理由です。

ここに私の質問をさらに説明するための不自然な例を示します。

public class HomeController : AsyncController
{
    // This method will work fine
    public async Task<ActionResult> ThisPageWillLoad()
    {
        // Do not await the task since it is meant to be fire and forget
        var task = this.FireAndForgetTask();

        return await Task.FromResult(this.View("Index"));
    }

    private async Task FireAndForgetTask()
    {
        var task = Task.Delay(TimeSpan.FromSeconds(3));
        await task;
    }

    // This method will throw the following exception:
    // System.InvalidOperationException: An asynchronous module or 
    // handler completed while an asynchronous operation was still pending
    public async Task<ActionResult> ThisPageWillNotLoad()
    {
        // Obviously can't await a void method
        this.FireAndForgetVoid();

        return await Task.FromResult(this.View("Index"));
    }

    private async void FireAndForgetVoid()
    {
        var task = Task.Delay(TimeSpan.FromSeconds(3));
        await task;
    }
}

関連するメモで、非同期ボイドの私の理解が正しい場合、ASP.Netは実際にそれを忘れていないので、このシナリオで非同期ボイドを「火と忘れ」と考えるのは間違っていませんか?

44
David Kreps

Microsoftは、asyncをASP.NETに取り込む際に、後方互換性の問題を可能な限り避けることを決定しました。そして、彼らはすべての「1つのASP.NET」にそれをもたらしたかったので、WinForms、MVC、WebAPI、SignalRなどのasyncサポート.

歴史的に、ASP.NETは、非同期コンポーネントがSynchronizationContextに開始と完了を通知するイベントベースの非同期パターン(EAP)を介して、.NET 2.0以降のクリーンな非同期操作をサポートしてきました。 .NET 4.5では、このサポートに最初のかなり大きな変更が加えられ、ASP.NETの非同期型のコアが更新され、タスクベースの非同期パターン(TAP、つまりasync)がより有効になりました。

それまでの間、異なるフレームワーク(WebForms、MVCなど)はすべて、後方互換性を優先して、そのコアと対話する独自の方法を開発しました。開発者を支援するために、ASP.NETのコアSynchronizationContextは、表示されている例外を除いて拡張されました。多くの使用ミスをキャッチします。

WebFormsの世界では、RegisterAsyncTaskがありますが、多くの人が代わりにasync voidイベントハンドラを使用しています。したがって、ASP.NET SynchronizationContextは、ページライフサイクルの適切なタイミングでasync voidを許可し、不適切なタイミングで使用すると、その例外が発生します。

MVC/WebAPI/SignalRの世界では、フレームワークはサービスとしてより構造化されています。したがって、彼らは非常に自然な方法でasync Taskを採用することができ、フレームワークは返されたTaskのみを扱う必要があります-非常にきれいな抽象化です。補足として、AsyncControllerはもう必要ありません。 MVCは、Taskを返すという理由だけで非同期であることを認識しています。

ただし、Task and を返そうとすると、async voidを使用しますが、これはサポートされていません。そして、それをサポートする理由はほとんどありません。とにかくそうすることを想定していないユーザーをサポートするだけでは、かなり複雑になります。 async voidは、MVCフレームワークを完全にバイパスして、コアASP.NET SynchronizationContextに直接通知することに注意してください。 MVCフレームワークはTaskを待つ方法を理解していますが、async voidについても知らないため、ASP.NETコアに完了を返し、実際には完了。

これは、2つのシナリオで問題を引き起こす可能性があります。

  1. async voidを使用するライブラリまたはその他を使用しようとしています。申し訳ありませんが、明白な事実は、ライブラリが壊れており、修正する必要があるということです。
  2. EAPコンポーネントをTaskにラップし、awaitを適切に使用しています。 EAPコンポーネントはSynchronizationContextと直接対話するため、これにより問題が発生する可能性があります。この場合、最良の解決策は、TAPを自然にサポートするように型を変更するか、TAP型(たとえば、HttpClientではなくWebClient)に置き換えることです。それに失敗すると、TAP-over-EAPの代わりにTAP-over-APMを使用できます。どちらも実行できない場合は、TAP-over-EAPラッパーでTask.Runを使用するだけで済みます。

「火と忘れ」について:

私は個人的にこのフレーズをasync voidメソッドに使用することはありません。一つには、エラー処理のセマンティクスは、「火事と忘却」というフレーズにはほとんど確実に適合しません。私は冗談でasync voidメソッドを "fire and crash"と呼んでいます。真のasync "fire and forget"メソッドは、返されるTaskを待つのではなく無視するasync Taskメソッドになります。

つまり、ASP.NETでは、リクエストから早期に戻りたくないことはほとんどありません(これは、 "fire and forget"が意味するものです)。この答えはすでに長すぎますが、本当に必要な場合は ASP.NET "fire and forget"をサポートするコードとともに、私のブログの問題の説明 があります。

58
Stephen Cleary