非常に単純な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:非同期操作がまだ保留中に非同期モジュールまたはハンドラーが完了しました。]
なぜ起こるのですか?私はいくつかのテストを行いました:
task = DownloadAsync();
を削除してIndex
メソッドに入れると、エラーなしで正常に機能します。DownloadAsync()
body return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
を使用すると、正常に機能します。コントローラーのコンストラクター内で_WebClient.DownloadStringTaskAsync
_メソッドを使用できないのはなぜですか?
非同期ボイド、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
克服しようとします。
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
メソッドを呼び出してから、元の値を復元します 。
関連する問題に遭遇しました。クライアントは、タスクを返すインターフェイスを使用しており、非同期で実装されています。
Visual Studio 2015では、非同期で、メソッドの呼び出し時にawaitキーワードを使用しないクライアントメソッドは警告やエラーを受け取らず、コードは正常にコンパイルされます。競合状態が実稼働環境に昇格します。
メソッド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();
}
メソッドが_async Task
_を返し、ConfigureAwait(false)
が解決策の1つになります。非同期voidのように動作し、同期コンテキストを続行しません(メソッドの最終結果に本当に関心がない限り)
同様の問題がありましたが、CancellationTokenをasyncメソッドのパラメーターとして渡すことで解決しました。