web-dev-qa-db-ja.com

同期コードを非同期呼び出しにラップする

ASP.NETアプリケーションには、完了するまでにかなりの時間がかかるメソッドがあります。このメソッドの呼び出しは、ユーザーが提供するキャッシュの状態とパラメーターに応じて、1回のユーザー要求中に最大3回発生する場合があります。各呼び出しが完了するまでに約1〜2秒かかります。メソッド自体はサービスへの同期呼び出しであり、実装をオーバーライドする可能性はありません。
したがって、サービスの同期呼び出しは次のようになります。

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

メソッドの使用法は次のとおりです(メソッドの呼び出しは複数回発生する可能性があることに注意してください)。

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

私はこの方法の同時作業を提供するために私の側から実装を変更しようとしましたが、ここに私がこれまで来たものがあります。

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

使用法(「他の処理を行う」コードの一部は、サービスの呼び出しと同時に実行されます):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

私の質問は次のとおりです。適切なアプローチを使用してASP.NETアプリケーションの実行を高速化しますか、または同期コードを非同期に実行しようとする不要なジョブを実行していますか? 2番目のアプローチがASP.NETのオプションではない理由を説明できますか(本当にそうでない場合)?また、そのようなアプローチが適用可能な場合、現時点で実行する唯一の呼び出しである場合、そのようなメソッドを非同期に呼び出す必要がありますか(完了を待っている間に他に何もする必要がない場合)?
このトピックに関するネットの記事のほとんどは、既にawaitableメソッドを提供しているコードでのasync-awaitアプローチの使用をカバーしていますが、それは私の場合ではありません。 ここ は私のケースを説明するニースの記事で、パラレルコールの状況を説明せず、同期コールをラップするオプションを拒否していますが、私の意見では、まさに私の状況がそれを行う機会です。
ヘルプとヒントを事前に感謝します。

75
Eadel

2つの異なるタイプの並行性を区別することが重要です。 非同期並行性とは、複数の非同期操作が実行中の場合です(各操作は非同期であるため、実際にはthreadを使用していません)。 並列並行性は、複数のスレッドがそれぞれ個別の操作を実行している場合です。

最初に行うことは、この仮定を再評価することです。

メソッド自体はサービスへの同期呼び出しであり、実装をオーバーライドする可能性はありません。

「サービス」がwebサービスまたはI/Oバウンドである他の何かである場合、最善の解決策は、そのための非同期APIを記述することです。

「サービス」は、Webサーバーと同じマシンで実行する必要があるCPUにバインドされた操作であるという前提で進めます。

その場合、次に評価することは別の仮定です。

より高速に実行するためのリクエストが必要です。

それがあなたがする必要があることを絶対に確信していますか?代わりに行うことができるフロントエンドの変更はありますか-たとえば、リクエストを開始し、処理中にユーザーが他の作業を行えるようにしますか?

はい、あなたは本当に個々のリクエストをより速く実行する必要があるという仮定を進めます。

この場合、Webサーバーで並列コードを実行する必要があります。 ASP.NETが他の要求を処理する必要がある可能性のあるスレッドを並列コードが使用し、スレッドを削除/追加することでASP.NETスレッドプールヒューリスティックがスローされるため、これは一般的にお勧めできません。したがって、この決定はサーバー全体に影響を及ぼします。

ASP.NETで並列コードを使用する場合、Webアプリのスケーラビリティを実際に制限する決定を下しています。また、特に要求がバースト的である場合は、かなりの量のスレッドチャーンが発生する場合があります。 know同時ユーザーの数が非常に少ない(つまり、パブリックサーバーではない)場合にのみ、ASP.NETで並列コードを使用することをお勧めします。

そのため、これまでのところ、ASP.NETで並列処理を実行したい場合は、いくつかのオプションがあります。

より簡単な方法の1つは、既存のコードに非常によく似たTask.Runを使用することです。ただし、CalculateAsyncメソッドを実装することはお勧めしません。これは、処理が非同期であることを示唆しているためです(非同期ではありません)。代わりに、呼び出しの時点でTask.Runを使用します。

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

または、コードで適切に機能する場合は、Parallelタイプ、つまりParallel.ForParallel.ForEach、またはParallel.Invokeを使用できます。 Parallelコードの利点は、要求スレッドが並列スレッドの1つとして使用され、スレッドコンテキストでの実行を再開することです(asyncの例よりもコンテキストの切り替えが少ない)。

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

ASP.NETでParallel LINQ(PLINQ)を使用することはまったくお勧めしません。

95
Stephen Cleary