web-dev-qa-db-ja.com

ASP.NET Controller:非同期操作がまだ保留中に非同期モジュールまたはハンドラーが完了しました

非常に単純なASP.NET MVC 4コントローラーがあります。

_public class HomeController : Controller
{
    private const string MY_URL = "http://smthing";
    private readonly Task<string> task;

    public HomeController() { task = DownloadAsync(); }

    public ActionResult Index() { return View(); }

    private async Task<string> DownloadAsync()
    {
        using (WebClient myWebClient = new WebClient())
            return await myWebClient.DownloadStringTaskAsync(MY_URL)
                                    .ConfigureAwait(false);
    }
}
_

プロジェクトを開始すると、ビューが表示され、見た目はきれいに見えますが、ページを更新すると、次のエラーが表示されます。

[InvalidOperationException:非同期操作がまだ保留中に非同期モジュールまたはハンドラーが完了しました。]

なぜ起こるのですか?私はいくつかのテストを行いました:

  1. コンストラクタからtask = DownloadAsync();を削除してIndexメソッドに入れると、エラーなしで正常に機能します。
  2. 別のDownloadAsync() body return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });を使用すると、正常に機能します。

コントローラーのコンストラクター内で_WebClient.DownloadStringTaskAsync_メソッドを使用できないのはなぜですか?

41
dyatchenko

非同期ボイド、ASP.Net、および未処理操作の数 では、Stephan Clearyがこのエラーの原因を説明しています。

歴史的に、ASP.NETは、非同期コンポーネントがその開始と完了をSynchronizationContextに通知するイベントベースの非同期パターン(EAP)を介して.NET 2.0以降のクリーンな非同期操作をサポートしてきました。

起こっているのは、クラスコンストラクター内でDownloadAsyncを起動していることです。非同期HTTP呼び出しでawaitが呼び出されています。これにより、非同期操作がASP.NET SynchronizationContextに登録されます。 HomeControllerが戻ると、まだ完了していない保留中の非同期操作があることがわかります。そのため、例外が発生します。

Task = DownloadAsync()を削除した場合;コンストラクターからIndexメソッドに入れると、エラーなしで正常に動作します。

上で説明したように、それはコントローラーから戻る間、保留中の非同期操作が実行されなくなったためです。

別のDownloadAsync()本体を使用してawait Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });を返すと、正常に機能します。

Task.Factory.StartNewがASP.NETで何か危険なことをするからです。 ASP.NETでタスクの実行を登録しません。これにより、プールリサイクルが実行され、バックグラウンドタスクが完全に無視され、異常終了が発生するEdgeケースが発生する可能性があります。 HostingEnvironment.QueueBackgroundWorkItem などのタスクを登録するメカニズムを使用する必要があるのはそのためです。

それが、あなたがやっていることを、あなたがそれをしている方法で行うことができない理由です。これを "fire-and-forget"スタイルでバックグラウンドスレッドで実行する場合は、HostingEnvironment(.NET 4.5.2を使用している場合)または BackgroundTaskManager 。これを行うことにより、スレッドプールスレッドを使用して非同期IO操作を行うことに注意してください。これは冗長であり、async IO with async-await克服しようとします。

50
Yuval Itzchakov

ASP.NETは、SynchronizationContextにバインドされた「非同期操作」を開始し、開始されたすべての操作が完了する前にActionResultを返すことは違法であると見なします。すべてのasyncメソッドは「非同期操作」として登録されるため、SynchronizationContextを返す前に、ASP.NET ActionResultにバインドするすべての呼び出しが完了していることを確認する必要があります。

コードでは、DownloadAsync()が最後まで実行されていることを確認せずに戻ります。ただし、結果をtaskメンバーに保存するため、これが完全であることを確認するのは非常に簡単です。返す前に、(非同期化した後)すべてのアクションメソッドに_await task_を配置するだけです。

_public async Task<ActionResult> IndexAsync()
{
    try
    {
        return View();
    }
    finally
    {
        await task;
    }
}
_

編集:

場合によっては、ASP.NETに戻る前に完了しないasyncメソッドを呼び出す必要があります。たとえば、バックグラウンドサービスタスクを遅延初期化すると、現在の要求が完了した後も実行を継続できます。 OPは、戻る前にタスクを完了させたいため、OPのコードには当てはまりません。ただし、タスクを開始する必要があり、タスクを待つ必要がない場合は、これを行う方法があります。単に、現在の_SynchronizationContext.Current_から「エスケープ」する手法を使用する必要があります。

  • 非推奨Task.Run()の1つの機能は、現在の同期コンテキストをエスケープすることです。ただし、ASP.NETのスレッドプールは特別であるため、ASP.NETでこれを使用しないことを推奨します。また、ASP.NETの外部であっても、このアプローチでは余分なコンテキストスイッチが発生します。

  • recommended)追加のコンテキスト切り替えを強制したり、ASP.NETのスレッドプールをすぐに煩わしたりせずに、現在の同期コンテキストをエスケープする安全な方法は set _SynchronizationContext.Current_をnullに変更し、asyncメソッドを呼び出してから、元の値を復元します

4
binki

関連する問題に遭遇しました。クライアントは、タスクを返すインターフェイスを使用しており、非同期で実装されています。

Visual Studio 2015では、非同期で、メソッドの呼び出し時にawaitキーワードを使用しないクライアントメソッドは警告やエラーを受け取らず、コードは正常にコンパイルされます。競合状態が実稼働環境に昇格します。

2
Beans

メソッドmyWebClient.DownloadStringTaskAsyncは別のスレッドで実行され、ブロックされません。可能な解決策は、myWebClientのDownloadDataCompletedイベントハンドラーとSemaphoreSlimクラスフィールドを使用してこれを行うことです。

private SemaphoreSlim signalDownloadComplete = new SemaphoreSlim(0, 1);
private bool isDownloading = false;

....

//Add to DownloadAsync() method
myWebClient.DownloadDataCompleted += (s, e) => {
 isDownloading = false;
 signalDownloadComplete.Release();
}
isDownloading = true;

...

//Add to block main calling method from returning until download is completed 
if (isDownloading)
{
   await signalDownloadComplete.WaitAsync();
}
1
Dan Randolph

メソッドが_async Task_を返し、ConfigureAwait(false)が解決策の1つになります。非同期voidのように動作し、同期コンテキストを続行しません(メソッドの最終結果に本当に関心がない限り)

1
code4j

同様の問題がありましたが、CancellationTokenをasyncメソッドのパラメーターとして渡すことで解決しました。

0
mko