web-dev-qa-db-ja.com

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

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

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

_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ステートメントは完了し、ラムダ関数は終了しますが、GetTotalAsyncの_return result;_行に到達することはありません。

私が間違っていることを知っていますか?

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

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

95
Keith

はい、それは大丈夫です。 TPLのよくある間違いですので、気を悪くしないでください。

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

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

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

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

これは、TPLを使用する重要なルールを示しています。.Result UIスレッド(または他の派手な同期コンテキスト)では、タスクが依存するものが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 "パターン。非同期修飾子を削除し、タスクオブジェクトを直接渡すだけです。他に何もないとしても、オーバーヘッドは少なくなります。

オプション#3は、待機をUIスレッドにスケジュールし直さずに、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の結果を待つと、現在のコンテキストは無視され、常にスレッドプールにスケジュールされます。これの欠点は、これを振りかける必要があることですeverywhereあなたの.Resultが依存するすべての関数で、.ConfigureAwaitは、別のデッドロックの原因である可能性があります。

139

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

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

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

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

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

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

11
Danilow