1. ホーム
  2. c#

[解決済み] タスクのResultプロパティにアクセスしようとすると、なぜこの非同期アクションがハングアップするのですか?

2022-10-01 01:36:42

質問

多階層の .Net 4.5 アプリケーションで、C# の新しいメソッドである 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 の行に到達することはありません。

何が間違っているのか、何か思い当たることはありますか?

これを解決するために、どこを調査する必要があるか、何か提案はありますか?

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

どのように解決するのですか?

はい、これはデッドロックです。TPLではよくあることなので、気にしないでください。

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

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

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

つまり、最初の行は非同期処理を開始するものです。次に2行目 はUIスレッドをブロックします。 . 従って、ランタイムが "return result" 行を UI スレッドに戻して実行しようとするとき、それは Result が完了するまで実行できません。しかし、もちろん、returnが起こるまでResultを与えることはできません。デッドロックです。

これは、TPLを使用する際の重要なルールを示しています。 .Result をUIスレッド(または他の派手な同期コンテキスト)で使用する場合、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 は別のデッドロックの原因となる可能性があるからです。