web-dev-qa-db-ja.com

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

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

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

私は多くの読書をしました、そして、これが何度も尋ねられて、答えられました。

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

非同期操作を同期的に待機し、Wait()がプログラムをここでフリーズする理由

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

非同期Task <T>メソッドを同期的に実行するにはどうすればよいですか?

非同期メソッドの同期呼び出し

C#の同期メソッドから非同期メソッドを呼び出す方法

問題は、ほとんどの答えが異なることです!私が見た最も一般的なアプローチは.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;
    }
58
Erick T

そのため、非同期メソッドを同期的に呼び出すのに最適な方法が残っています。

まず、これはやるべきことです。スタックオーバーフローでは、具体的なケースに関係なく、包括的なステートメントとして悪魔の行為としてこれを指摘するのが一般的であるため、これを述べています。

完全に非同期である必要はありません正確さのため。非同期の何かをブロックして同期させると、パフォーマンスコストが問題になるか、まったく無関係になる可能性があります。具体的なケースに依存します。

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

ここでは、.ConfigureAwait(false)へのすべての呼び出しは、待機していないので無意味です。

すべてのタスクをそのように処理できるわけではないため、RunSynchronouslyの使用は無効です。

.GetAwaiter().GetResult()は、await例外伝播動作を模倣するという点でResult/Wait()と異なります。あなたがそれを望むかどうかを決める必要があります。 (したがって、その動作が何であるかを調査します。ここで繰り返す必要はありません。)

それに加えて、これらのアプローチはすべて同様のパフォーマンスを備えています。 OSイベントを何らかの方法で割り当て、ブロックします。それは高価な部分です。どちらのアプローチが絶対に安いのかわかりません。

私は個人的にTask.Run(() => DoSomethingAsync()).Wait();パターンが好きです。それは、デッドロックを確実に回避し、シンプルで、GetResult()が隠すかもしれないいくつかの例外を隠さないからです。ただし、GetResult()もこれと一緒に使用できます。

72
usr

.NET 3.5で構築されたAPIサーフェスを持つライブラリを更新中です。その結果、すべてのメソッドは同期的です。 APIを変更することはできません(つまり、戻り値をTaskに変換します)。すべての呼び出し元を変更する必要があるためです。そのため、非同期メソッドを同期的に呼び出すのに最適な方法が残っています。

Sync-over-asyncアンチパターンを実行するための普遍的な「最良の」方法はありません。それぞれに欠点があるさまざまなハックのみ。

私がお勧めするのは、古い同期APIを保持してから、非同期APIをそれらと一緒に導入することです。これは、MSDNのBrownfield Asyncに関する記事に記載されている "boolean argument hack" を使用して実行できます。

まず、例の各アプローチの問題について簡単に説明します。

  1. ConfigureAwaitは、awaitがある場合にのみ意味があります。そうでなければ、何もしません。
  2. Resultは、例外をAggregateExceptionにラップします。ブロックする必要がある場合は、代わりにGetAwaiter().GetResult()を使用してください。
  3. Task.Runは、スレッドプールスレッドでコードを実行します(明らかに)。これは問題ありませんコードの場合にのみcanスレッドプールスレッドで実行します。
  4. RunSynchronouslyは、動的なタスクベースの並列処理を行う非常にまれな状況で使用される高度なAPIです。あなたはそのシナリオにはまったく入っていません。
  5. 単一タスクを持つTask.WaitAllは、単にWait()と同じです。
  6. async () => await x() => xと言うのはあまり効率的ではありません。
  7. 現在のスレッドから開始されたタスクのブロック デッドロックを引き起こす可能性があります

内訳は次のとおりです。

// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (2), (3)
result = Task.Run(task).Result;

// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();

// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;

// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;

これらのアプローチの代わりに、既存の動作する同期コードがあるため、新しい自然非同期コードと一緒に使用する必要があります。たとえば、既存のコードでWebClientを使用した場合:

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

非同期APIを追加する場合は、次のようにします。

private async Task<string> GetCoreAsync(bool sync)
{
  using (var client = new WebClient())
  {
    return sync ?
        client.DownloadString(...) :
        await client.DownloadStringTaskAsync(...);
  }
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

または、must何らかの理由でHttpClientを使用する場合:

private string GetCoreSync()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

private static HttpClient HttpClient { get; } = ...;

private async Task<string> GetCoreAsync(bool sync)
{
  return sync ?
      GetCoreSync() :
      await HttpClient.GetString(...);
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

このアプローチでは、ロジックはCoreメソッドに入ります。このメソッドは、同期的にまたは非同期に(syncパラメーターによって決定されるように)実行されます。 synctrueの場合、コアメソッドmustは既に完了したタスクを返します。実装については、同期APIを使用して同期的に実行し、非同期APIを使用して非同期的に実行します。

最終的には、同期APIを廃止することをお勧めします。

31
Stephen Cleary