C#のasync
/await
キーワードを使用した非同期プログラミングのベストプラクティスに出会いました(c#5.0が初めてです)。
与えられたアドバイスの1つは次のとおりです。
安定性:同期コンテキストを知る
...一部の同期コンテキストは、リエントラントではなく、シングルスレッドです。つまり、特定の時間にコンテキストで実行できる作業単位は1つだけです。この例は、Windows UIスレッドまたはASP.NET要求コンテキストです。これらのシングルスレッドの同期コンテキストでは、簡単にデッドロックが発生します。シングルスレッドコンテキストからタスクを生成し、コンテキストでそのタスクを待機する場合、待機コードがバックグラウンドタスクをブロックしている可能性があります。
_public ActionResult ActionAsync()
{
// DEADLOCK: this blocks on the async task
var data = GetDataAsync().Result;
return View(data);
}
private async Task<string> GetDataAsync()
{
// a very simple async method
var result = await MyWebService.GetDataAsync();
return result.ToString();
}
_
自分で分析しようとすると、メインスレッドはMyWebService.GetDataAsync();
で新しいスレッドを生成しますが、メインスレッドはそこで待機するため、GetDataAsync().Result
で結果を待機します。一方、データの準備ができていると言います。メインスレッドが継続ロジックを継続せず、GetDataAsync()
から文字列の結果を返すのはなぜですか?
上記の例でデッドロックが発生する理由を誰かに説明していただけますか?私は問題が何であるかについて完全に無知です...
here の例をご覧ください。Stephenが明確な答えをくれました。
最上位のメソッド(Button1_Click for UI/MyController.Get for ASP.NET)から始めて、これが起こります。
トップレベルのメソッドは、GetJsonAsyncを(UI/ASP.NETコンテキスト内で)呼び出します。
GetJsonAsyncは、HttpClient.GetStringAsyncを呼び出して(まだコンテキスト内で)REST要求を開始します。
GetStringAsyncは、RESTリクエストが完了していないことを示す未完了のタスクを返します。
GetJsonAsyncは、GetStringAsyncによって返されるタスクを待機します。コンテキストがキャプチャされ、後でGetJsonAsyncメソッドの実行を継続するために使用されます。 GetJsonAsyncは、GetJsonAsyncメソッドが完了していないことを示す未完了のタスクを返します。
最上位のメソッドは、GetJsonAsyncによって返されたタスクを同期的にブロックします。これにより、コンテキストスレッドがブロックされます。
...最終的に、RESTリクエストが完了します。これにより、GetStringAsyncによって返されたタスクが完了します。
GetJsonAsyncの継続を実行する準備が整いました。コンテキストで実行できるように、コンテキストが利用可能になるのを待ちます。
デッドロック。最上位のメソッドは、コンテキストスレッドをブロックし、GetJsonAsyncが完了するのを待機しています。GetJsonAsyncは、コンテキストが解放されて完了できるように待機しています。 UIの例では、「コンテキスト」はUIコンテキストです。 ASP.NETの例では、「コンテキスト」はASP.NET要求コンテキストです。このタイプのデッドロックは、どちらの「コンテキスト」でも発生する可能性があります。
読むべき別のリンク:
GetDataAsync().Result;
は、GetDataAsync()
によって返されたタスクが完了すると実行され、その間、UIスレッドをブロックしますreturn result.ToString()
)の継続は、実行のためにUIスレッドのキューに入れられますGetDataAsync()
によって返されるタスクは、キューに入れられた継続が実行されると完了しますデッドロックは、ファクト1またはファクト2を回避するために提供された代替手段によって解除できます。
var data = await GetDataAsync()
を使用します。これにより、UIスレッドの実行を継続できますvar data = Task.Run(GetDataAsync).Result
を使用します。これは、スレッドプールスレッドの同期コンテキストに継続をポストします。これにより、GetDataAsync()
が返すタスクを完了できます。これは Stephen Toubの記事 で本当によく説明されており、DelayAsync()
の例を使用する半分ほど下にあります。
私はMVC.Netプロジェクトでこの問題をもう一度いじっていました。 PartialViewから非同期メソッドを呼び出したい場合、PartialViewを非同期にすることはできません。その場合、例外が発生します。
したがって、基本的に、syncメソッドからasyncメソッドを呼び出したいシナリオでの簡単な回避策は、次のことを実行できます。
例:
public ActionResult DisplayUserInfo(string userName)
{
// trick to prevent deadlocks of calling async method
// and waiting for on a sync UI thread.
var syncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
// this is the async call, wait for the result (!)
var model = _asyncService.GetUserInfo(Username).Result;
// restore the context
SynchronizationContext.SetSynchronizationContext(syncContext);
return PartialView("_UserInfo", model);
}
もう1つの重要な点は、タスクをブロックしてはならず、非同期を使用してデッドロックを防ぐことです。その後、同期ブロッキングではなく、すべて非同期になります。
public async Task<ActionResult> ActionAsync()
{
var data = await GetDataAsync();
return View(data);
}
private async Task<string> GetDataAsync()
{
// a very simple async method
var result = await MyWebService.GetDataAsync();
return result.ToString();
}
回避策は、結果を要求する前にタスクでJoin
拡張メソッドを使用することです。
コードは次のようになります。
public ActionResult ActionAsync()
{
var task = GetDataAsync();
task.Join();
var data = task.Result;
return View(data);
}
結合方法は次のとおりです。
public static class TaskExtensions
{
public static void Join(this Task task)
{
var currentDispatcher = Dispatcher.CurrentDispatcher;
while (!task.IsCompleted)
{
// Make the dispatcher allow this thread to work on other things
currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
}
}
}
私はこのソリューションの欠点を見るためにドメインに十分ではありません(もしあれば)