web-dev-qa-db-ja.com

デッドロックを引き起こす非同期/待機の例

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()から文字列の結果を返すのはなぜですか?

上記の例でデッドロックが発生する理由を誰かに説明していただけますか?私は問題が何であるかについて完全に無知です...

83
droritos

here の例をご覧ください。Stephenが明確な答えをくれました。

最上位のメソッド(Button1_Click for UI/MyController.Get for ASP.NET)から始めて、これが起こります。

  1. トップレベルのメソッドは、GetJsonAsyncを(UI/ASP.NETコンテキスト内で)呼び出します。

  2. GetJsonAsyncは、HttpClient.GetStringAsyncを呼び出して(まだコンテキスト内で)REST要求を開始します。

  3. GetStringAsyncは、RESTリクエストが完了していないことを示す未完了のタスクを返します。

  4. GetJsonAsyncは、GetStringAsyncによって返されるタスクを待機します。コンテキストがキャプチャされ、後でGetJsonAsyncメソッドの実行を継続するために使用されます。 GetJsonAsyncは、GetJsonAsyncメソッドが完了していないことを示す未完了のタスクを返します。

  5. 最上位のメソッドは、GetJsonAsyncによって返されたタスクを同期的にブロックします。これにより、コンテキストスレッドがブロックされます。

  6. ...最終的に、RESTリクエストが完了します。これにより、GetStringAsyncによって返されたタスクが完了します。

  7. GetJsonAsyncの継続を実行する準備が整いました。コンテキストで実行できるように、コンテキストが利用可能になるのを待ちます。

  8. デッドロック。最上位のメソッドは、コンテキストスレッドをブロックし、GetJsonAsyncが完了するのを待機しています。GetJsonAsyncは、コンテキストが解放されて完了できるように待機しています。 UIの例では、「コンテキスト」はUIコンテキストです。 ASP.NETの例では、「コンテキスト」はASP.NET要求コンテキストです。このタイプのデッドロックは、どちらの「コンテキスト」でも発生する可能性があります。

読むべき別のリンク:

待って、UI、デッドロック!ああ!

72
cuongle
  • 事実1:GetDataAsync().Result;は、GetDataAsync()によって返されたタスクが完了すると実行され、その間、UIスレッドをブロックします
  • 事実2:待機(return result.ToString())の継続は、実行のためにUIスレッドのキューに入れられます
  • 事実3:GetDataAsync()によって返されるタスクは、キューに入れられた継続が実行されると完了します
  • 事実4:UIスレッドがブロックされているため、キューに入れられた継続は実行されません(事実1)

デッドロック!

デッドロックは、ファクト1またはファクト2を回避するために提供された代替手段によって解除できます。

  • 1,4を避けてください。 UIスレッドをブロックする代わりに、var data = await GetDataAsync()を使用します。これにより、UIスレッドの実行を継続できます
  • 2,3を避けてください。待機の継続を、ブロックされていない別のスレッドのキューに入れます。 var data = Task.Run(GetDataAsync).Resultを使用します。これは、スレッドプールスレッドの同期コンテキストに継続をポストします。これにより、GetDataAsync()が返すタスクを完了できます。

これは Stephen Toubの記事 で本当によく説明されており、DelayAsync()の例を使用する半分ほど下にあります。

15
Phillip Ngan

私はMVC.Netプロジェクトでこの問題をもう一度いじっていました。 PartialViewから非同期メソッドを呼び出したい場合、PartialViewを非同期にすることはできません。その場合、例外が発生します。

したがって、基本的に、syncメソッドからasyncメソッドを呼び出したいシナリオでの簡単な回避策は、次のことを実行できます。

  1. 呼び出しの前に、SynchronizationContextをクリアします
  2. 呼び出しを行うと、ここでデッドロックはなくなり、終了するのを待ちます
  3. synchronizationContextを復元します

例:

    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);
    }
11
Herre Kuijpers

もう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();
}
2
marvelTracker

回避策は、結果を要求する前にタスクで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);
        }
    }
}

私はこのソリューションの欠点を見るためにドメインに十分ではありません(もしあれば)

0
Orace