114

asyncC# の new キーワードとキーワードを使用してメソッドを呼び出している多層 .Net 4.5 アプリケーションawaitがハングするだけで、その理由がわかりません。

一番下には、データベース ユーティリティを拡張する async メソッドがあります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は到達しません。

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

これを修正するためにどこを調査する必要があるかについて何か提案はありますか?

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

4

5 に答える 5

161

はい、デッドロックです。そして、TPL でよくある間違いなので、気を悪くしないでください。

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

では、このデッドロックはどのように発生するのでしょうか? 次のコードがあると想像してください。

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

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

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

それで、あなたは何をしますか?オプション #1 はどこでも await を使用することですが、あなたが言ったように、それはすでにオプションではありません。利用可能な 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あることです。

于 2013-01-25T17:27:56.163 に答える
40

これは、ブログで説明しているように、古典的な混合asyncデッドロックシナリオです。ジェイソンはそれをよく説明しました。デフォルトでは、「コンテキスト」は毎回保存され、メソッドを続行するために使用されます。この「コンテキスト」は、それがない限り現在のものであり、そうでない場合は現在のものです。メソッドが続行しようとすると、最初にキャプチャされた「コンテキスト」(この場合はASP.NET)に再入力します。ASP.NETは、コンテキスト内で一度に1つのスレッドのみを許可し、コンテキスト内にはすでにスレッドがあります-スレッドはでブロックされています。awaitasyncSynchronizationContextnullTaskSchedulerasyncSynchronizationContextSynchronizationContextTask.Result

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

  1. asyncずっと下に使用してください。あなたはこれを「できない」と言っていますが、なぜそうしないのかわかりません。.NET4.5上のASP.NETMVCは確かasyncにアクションをサポートでき、変更を加えるのは難しいことではありません。
  2. ConfigureAwait(continueOnCapturedContext: false)可能な限り使用してください。これは、キャプチャされたコンテキストで再開するデフォルトの動作を上書きします。
于 2013-01-25T18:30:28.733 に答える
15

私は同じデッドロックの状況にありましたが、同期メソッドから非同期メソッドを呼び出す私の場合、私にとってうまくいくのは次のとおりです。

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

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

于 2016-11-13T00:57:30.077 に答える
4

受け入れられた回答に追加するために(コメントするのに十分な担当者がいません)、この例のように、以下task.Resultのすべてのイベントを使用してブロックすると、この問題が発生しました。awaitConfigureAwait(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);
}
于 2015-12-22T19:36:43.097 に答える