1. ホーム
  2. c#

[解決済み] 非同期コードからの非同期メソッドの呼び出し

2023-05-25 14:04:13

質問

.NET 3.5 で構築された API サーフェスを持つライブラリを更新している最中です。その結果、すべてのメソッドは同期です。API を変更する (つまり、戻り値をタスクに変換する) ことはできません。そのため、非同期メソッドを同期的に呼び出すにはどうしたらよいかという問題が残されています。これは、ASP.NET 4、ASP.NET Core、および .NET/.NET Core コンソール アプリケーションのコンテキストでの話です。

私は十分に明確ではなかったかもしれません。状況は、非同期を認識していない既存のコードを持っており、System.Net.Http や AWS SDK などの非同期メソッドのみをサポートする新しいライブラリを使用したい、というものです。したがって、私はギャップを埋める必要があり、同期的に呼び出すことができるコードを持つことができ、その後、他の場所で非同期メソッドを呼び出すことができます。

私はたくさん読みましたし、これは何度も質問され、回答されています。

非同期メソッドから非同期メソッドを呼び出す

非同期処理での同期待ち、ここでWait()がプログラムをフリーズさせる理由

同期メソッドから非同期メソッドを呼び出す

非同期Task<T>メソッドを同期的に実行するにはどうしたらよいでしょうか。

非同期メソッドを同期的に呼び出す

C#でsynchronousメソッドから非同期メソッドを呼び出すには?

問題は、ほとんどの回答が異なっていることです! 私が見た最も一般的なアプローチは、.Resultを使用することですが、これはデッドロックになる可能性があります。しかし、デッドロックを回避し、優れたパフォーマンスを発揮し、ランタイムとうまく機能する (タスク スケジューラー、タスク作成オプションなどを尊重するという意味で) 最良のアプローチがどれなのかがわかりません。決定的な答えはあるのでしょうか?最良のアプローチは何でしょうか?

private static T taskSyncRunner<T>(Func<Task<T>> task)
    {
        T result;
        // approach 1
        result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 2
        result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 3
        result = task().ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 4
        result = Task.Run(task).Result;

        // approach 5
        result = Task.Run(task).GetAwaiter().GetResult();


        // approach 6
        var t = task();
        t.RunSynchronously();
        result = t.Result;

        // approach 7
        var t1 = task();
        Task.WaitAll(t1);
        result = t1.Result;

        // approach 8?

        return result;
    }

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

<ブロッククオート

というわけで、非同期メソッドを同期的に呼び出すのに最適な方法が残されています。

まず、これはやってもいいことです。Stack Overflowでは、具体的なケースを無視して一律に悪魔の所業として指摘するのが一般的なので、このように記載しています。

ずっと非同期である必要はない 正しさのため . 同期させるために非同期のものをブロックすることは、重要かもしれないし全く関係ないかもしれない性能上のコストがあります。それは具体的なケースに依存します。

デッドロックは、2 つのスレッドが同じシングルスレッド同期コンテキストに同時に入ろうとすることから発生します。これを確実に回避する技術は、ブロッキングによって引き起こされるデッドロックを回避することができます。

あなたのコードスニペットでは、すべての呼び出しが .ConfigureAwait(false) の呼び出しは、戻り値が期待できないため、無意味です。 ConfigureAwait は、待ち受けにしたときに要求された動作をする構造体を返します。その構造体が単にドロップされただけなら、何もしません。

RunSynchronously は、すべてのタスクがそのように処理できるわけではないので、使用するのは無効です。この方法はCPUベースのタスクのためのものであり、特定の状況下では動作しないことがあります。

.GetAwaiter().GetResult() とは異なり Result/Wait() を模倣している点で await 例外伝播の振る舞いを模倣しています。それを望むか望まないかを決める必要があります。(だから、その動作が何であるか調べてください。ここで繰り返す必要はありません。) もしタスクが一つの例外を含んでいるならば await エラー動作は通常便利で、デメリットはほとんどありません。複数の例外がある場合、例えば失敗した Parallel ループで複数のタスクが失敗した場合など、複数の例外が発生する場合は await は最初の例外を除いて全ての例外を落とします。そのため、デバッグがしにくくなります。

これらのアプローチはすべて似たようなパフォーマンスです。これらは、何らかの方法で OS イベントを割り当て、その上でブロックします。これが高価な部分です。他の機械はそれに比べればむしろ安いものです。どのアプローチが絶対的に一番安いかはわかりません。

例外がスローされる場合、それが最も高価な部分となる予定です。.NET 5 では、例外は高速な CPU でせいぜい 200,000 件/秒の割合で処理されます。ディープスタックは遅く、タスク機械は例外を再スローする傾向があり、そのコストは倍増します。例外を投げ直さずにタスクでブロックする方法があります。 task.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously).Wait(); .

個人的に好きなのは Task.Run(() => DoSomethingAsync()).Wait(); パターンは、デッドロックを断固として回避し、シンプルで、かつ GetResult() が隠すかもしれない例外を隠さないからです。しかし GetResult() を使うこともできます。