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


94

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()ですか?

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


GetDataAsyncが完了したことを本当に確信していますか?それともスタックして、ロックだけでデッドロックは発生しますか?
Andrey

これは提供された例です。私の理解では、それはものを終えて、ある種の結果の準備ができているはずです...
Dror Weiss

4
なぜあなたはその仕事を待っているのですか?あなたは基本的に非同期モデルのすべての利点を失ったので、代わりにあなたは待つべきです。
Toni Petrina 2013

@ToniPetrinaのポイントを追加すると、デッドロックの問題がなくても、ブロックすべきではないコンテキスト(UIまたはASP.NET要求)では決してvar data = GetDataAsync().Result;実行してはならないコード行です。デッドロックが発生しなくても、スレッドが不確定な時間ブロックされます。 つまり、基本的にはひどい例です。[そのようなコードを実行する前にUIスレッドを終了するかawait、Toniが示唆するようにそこで使用する必要があります。]
ToolmakerSteve

回答:


81

この例を見てください。Stephenは明確な答えを持っています。

つまり、これがトップレベルのメソッド(Button1_ClickUI / MyController.GetASP.NET用)から始まります。

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

  2. GetJsonAsync呼び出しによってRESTリクエストを開始しますHttpClient.GetStringAsync(まだコンテキスト内にあります)。

  3. GetStringAsyncTaskRESTリクエストが完了していないことを示すuncompletedを返します。

  4. GetJsonAsyncからのTask返却を待っていGetStringAsyncます。コンテキストがキャプチャされ、GetJsonAsync後でメソッドの実行を続行するために使用されます。メソッドが完了していないことを示すGetJsonAsyncuncompletedを返します。TaskGetJsonAsync

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

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

  7. の継続GetJsonAsyncが実行可能になり、コンテキストが使用可能になるのを待って、コンテキストで実行できるようになります。

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

あなたが読むべきもう一つのリンク:待って、そしてUI、そしてデッドロック!あら!


20
  • 事実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は避けてください。ブロックされていない別のスレッドへの待機の継続をキューに入れます(例:use var data = Task.Run(GetDataAsync).Result)。これにより、継続がスレッドプールスレッドの同期コンテキストにポストされます。これにより、によって返されたタスク GetDataAsync()を完了できます。

これは、Stephen Toubの記事で、彼がの例を使用しているところの半分ほど下で本当によく説明されていDelayAsync()ます。


に関して、var data = Task.Run(GetDataAsync).Resultそれは私にとって新しいです。私はいつもアウター.Resultが最初のウェイトGetDataAsyncがヒットするとすぐにすぐに利用できるようになると常に思っていたので、data常にそうなりますdefault。面白い。
ナウファル

19

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

asyncsyncメソッドからメソッドを呼び出すシナリオでは、次の簡単な回避策を使用できます。

  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);
}

3

もう1つの重要な点は、タスクをブロックしないでください。また、デッドロックを防ぐために、asyncをずっと使用する必要があります。次に、それはすべて同期ブロッキングではなく非同期になります。

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();
}

6
私があれば何をしたいのメイン(UI)スレッドがタスクが終了するまで、ブロックしますか?または、例えばコンソールアプリで?非同期のみをサポートするHttpClientを使用したいとしましょう... デッドロックのリスクなしで同期的に使用するにはどうすればよいですか?これ可能でなければなりません。(同期メソッドがあるため)WebClientがそのように使用でき、完全に機能する場合、なぜHttpClientでもそれができないのでしょうか。
デクスター

上記のPhilip Nganからの回答を参照してください(これはこのコメントの後に投稿されたことを知っています):ブロックされていない別のスレッドへの待機の継続をキューに入れます。例:var data = Task.Run(GetDataAsync).Result
Jeroen

@Dexter-re「タスクが完了するまでメイン(UI)スレッドをブロックしたい場合はどうしますか?」-UIスレッドを本当にブロックしますか?つまり、ユーザーは何もできず、キャンセルもできません-または現在の方法を続行したくないということですか?「await」または「Task.ContinueWith」は後者のケースを処理します。
ToolmakerSteve

@ToolmakerSteveもちろん、メソッドを続行したくありません。しかし、私は単純にすることはできません、私はどちらかのすべての方法非同期(async)を使用することができないためのawait使用-のHttpClientがで呼び出され、メイン、もちろん非同期ことができません。この場合には、私は正確に元が欲しい- -そして、私はコンソールアプリでこのすべてをやって言及した私は私のアプリでもしたくないこと、マルチスレッド。すべてをブロックします
デクスター

-1

私が行った回避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);
        }
    }
}

このソリューションの欠点を確認するのに十分ではありません(存在する場合)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.