なぜこの非同期アクションがハングするのですか?


102

私はC#の新しい使用法呼び出すマルチティアの.Net 4.5アプリケーション持つasyncawaitそのキーワードだけでハングアップし、私はなぜ見ることができません。

一番下には、データベースユーティリティを拡張する非同期メソッドがありますOurDBConn(基本的には、基になるオブジェクトDBConnectionDBCommandオブジェクトのラッパー)。

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

次に、これを呼び出して遅い合計を取得する中レベルの非同期メソッドがあります:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

最後に、同期的に実行されるUIメソッド(MVCアクション)があります。

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

問題は、それがその最後の行に永遠に掛かることです。呼び出しても同じことですasyncTask.Wait()。遅いSQLメソッドを直接実行すると、約4秒かかります。

私が期待している振る舞いは、に到達したときにasyncTask.Result、終了していない場合は終了するまで待機し、終了したら結果を返すことです。

デバッガーでステップスルーすると、SQLステートメントは完了し、ラムダ関数は終了しますが、return result;行にGetTotalAsync到達しません。

私が間違っていることは何か考えていますか?

これを修正するために調査する必要がある場所への提案はありますか?

これはどこかでデッドロックになる可能性がありますか?そうであれば、それを見つける直接的な方法はありますか?

回答:


150

うん、それは大丈夫デッドロックだ。TPLによくある間違いなので、気を悪くしないでください。

を記述するawait fooと、ランタイムはデフォルトで、メソッドが開始したのと同じSynchronizationContextで関数の継続をスケジュールします。英語でExecuteAsync、UIスレッドからを呼び出したとしましょう。(を呼び出したためTask.Run)クエリはスレッドプールスレッドで実行されますが、その結果を待ちます。これは、ランタイムが「return result;」行をスレッドプールにスケジュールするのではなく、UIスレッドで実行するようにスケジュールすることを意味します。

では、このデッドロックはどのように行われるのでしょうか。あなたがこのコードを持っていると想像してください:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

したがって、最初の行は非同期作業を開始します。2行目は、UIスレッドをブロックします。そのため、ランタイムがUIスレッドで「結果を返す」行を実行したい場合は、Result完了するまで実行できません。ただし、もちろん、結果が返されるまで結果を返すことはできません。デッドロック。

これは、TPLを使用する際の重要なルールを示しています。UI .Resultスレッド(またはその他の高度な同期コンテキスト)で使用する場合は、タスクが依存するものが何もUIスレッドにスケジュールされないように注意する必要があります。そうでなければ悪が起こります。

それで、あなたは何をしますか?オプション#1はどこでも使用できるのですが、あなたが言ったように、それはすでにオプションではありません。利用可能な2番目のオプションは、単にawaitの使用をやめることです。2つの関数を次のように書き換えることができます。

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

違いは何ですか?現在、待機している場所がないため、UIスレッドに暗黙的にスケジュールされているものはありません。単一の戻り値を持つこれらのような単純なメソッドの場合、 " var result = await...; return result"パターンを実行しても意味がありません。async修飾子を削除して、タスクオブジェクトを直接渡します。他に何もなければ、それはオーバーヘッドが少ないです。

オプション#3は、待機をUIスレッドにスケジュールするのではなく、スレッドプールにスケジュールすることを指定することです。これはConfigureAwait、次のようにメソッドで行います。

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

通常、タスクを待機している場合は、UIスレッドにスケジュールを設定します。の結果を待つと、現在のContinueAwaitコンテキストは無視され、常にスレッドプールにスケジュールされます。これの欠点は、.Resultが依存するすべての関数のどこにでもこれを振りかける必要があることです.ConfigureAwait


6
ところで、問題はASP.NETに関するものなので、UIスレッドはありません。ただし、ASP.NETのため、デッドロックの問題はまったく同じSynchronizationContextです。
2013年

問題はありませんが、async/ awaitキーワードなしでTPLを使用した同様の.Net 4コードがあったので、それは多くのことを説明しました。
キース


誰もがVB.netコードを探している場合は、それがここで説明されて(私のように):docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/...
MichaelDarkBlue


36

私のブログで説明したように、これは古典的な混合asyncデッドロックのシナリオです。Jasonはそれをうまく説明しました。デフォルトでは、「コンテキスト」はすべてに保存され、メソッドを続行するために使用されます。この「文脈」は、現在あること、それがない限り、それが現在である場合には、。場合メソッドの試みを継続するためには、第1の再入射する(この場合、ASP.NET捕捉「コンテキスト」)。ASP.NET は一度に1つのスレッドしかコンテキストに許可せず、コンテキストにすでにスレッドが存在します-スレッドはブロックされています。awaitasyncSynchronizationContextnullTaskSchedulerasyncSynchronizationContextSynchronizationContextTask.Result

このデッドロックを回避する2つのガイドラインがあります。

  1. 下まで使用asyncしてください。あなたはこれが「できない」と述べていますが、なぜそうしないのかわかりません。.NET 4.5上のASP.NET MVCは確かにasyncアクションをサポートでき、変更するのは難しいことではありません。
  2. ConfigureAwait(continueOnCapturedContext: false)できるだけ使用してください。これは、キャプチャされたコンテキストで再開するデフォルトの動作を上書きします。

ConfigureAwait(false)、現在の機能が異なる文脈に再開することを保証しますか?
キューx 2013

MVCフレームワークはこれをサポートしていますが、これは既存のMVCアプリの一部であり、多数のクライアント側JSがすでに存在しています。これがasyncクライアント側で機能する方法を壊さずに、アクションに簡単に切り替えることはできません。私は確かに長期的にそのオプションを調査する予定です。
キース

私のコメントを明確にするために- ConfigureAwait(false)コールツリーを下に使用することでOPの問題が解決されるかどうか知りたいと思いました。
キューx 2013

3
@キース:MVCアクションを実行asyncしても、クライアント側にはまったく影響しません。これについては、別のブログ記事async「Does't Change the HTTP Protocol」で説明しています
スティーブンクリアリー2013年

1
@キース:asyncコードベースを通じて「成長」することは正常です。コントローラメソッドが非同期操作に依存している可能性がある場合、基本クラスメソッドはを返す必要がありTask<ActionResult>ます。大規模なプロジェクトをに移行することasyncasync、コードのミキシングと同期が困難で扱いにくいため、常に厄介です。純粋なasyncコードははるかに単純です。
スティーブンクリアリー2013年

12

私は同じデッドロック状態にありましたが、私の場合、syncメソッドからasyncメソッドを呼び出すと、私にとってはうまくいきました:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

これは良いアプローチですか?


この解決策は私にとってもうまくいきますが、それが良い解決策であるかどこかで壊れるかどうかはわかりません。誰でもそれを説明できます
Konstantin Vdovkin 2017年

うまくついに.....私はこのソリューションに行き、それが問題なく生産的な環境で働いている
Danilow

1
Task.Runを使用してパフォーマンスヒットしていると思います。私のテストでは、Task.Runは100ミリ秒のHTTPリクエストの実行時間をほぼ倍にしています。
ティモシーゴンザレス

1
つまり、非同期呼び出しをラップするための新しいタスクを作成しています。パフォーマンスはトレードオフです
Danilow

これは素晴らしいことでした。私の場合も、非同期メソッドを呼び出す同期メソッドが原因でした。ありがとうございました!
Leonardo Spina

4

受け入れられた回答に追加するために(コメントするには十分な担当者ではありません)、以下の例のように、以下のtask.Resultすべてawaitがであったにもかかわらず、を使用してブロックするときにこの問題が発生しましたConfigureAwait(false)

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

問題は実際には外部ライブラリのコードにありました。非同期ライブラリメソッドは、待機をどのように構成しても、呼び出し同期コンテキストで続行しようとし、デッドロックを引き起こしました。

したがって、答えは、自分のバージョンの外部ライブラリコードをロールバックしExternalLibraryStringAsyncて、希望する継続プロパティを持つようにすることでした。


歴史的な目的のための間違った答え

多くの苦痛と苦痛の後、私はこのブログ投稿(「デッドロック」の場合はCtrl-f)に埋め込まれた解決策を見つけました。task.ContinueWithベアの代わりにを使用して展開しtask.Resultます。

以前のデッドロックの例:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

このようなデッドロックを回避します。

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

反対票は何ですか?このソリューションは私のために働いています。
Cameron Jeffers、2015

Taskが完了する前にオブジェクトを返しているため、返されたオブジェクトの変更が実際にいつ発生するかを呼び出し元に判断する手段がありません。
サービー

うーんそうです。それで、手動でブロックしているwhileループ(またはそのようなもの)を使用するある種の「タスクが完了するまで待機する」メソッドを公開する必要がありますか?または、そのようなブロックをGetFooSynchronousメソッドにパックしますか?
Cameron Jeffers

1
実行すると、デッドロックが発生します。Taskブロックするのではなく、を返すことで、完全に非同期にする必要があります。
Servy

残念ながらそれはオプションではありません、クラスは変更できない同期インターフェイスを実装しています。
Cameron Jeffers、2015

0

クイックアンサー:この行を変更

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

どうして?.resultを使用して、コンソールアプリケーションを除くほとんどのアプリケーション内のタスクの結果を取得しないでください。そうすると、プログラムがそこに到達したときにハングします。

.Resultを使用する場合は、以下のコードを試すこともできます。

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