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


95

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);
    });
}

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

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

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

回答:


116

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

まず最初に、この仮定を再評価します。

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

「サービス」がWebサービスまたはI / Oバウンドのその他のものである場合、最適なソリューションは、そのための非同期APIを作成することです。

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

その場合、次の評価は別の仮定です。

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

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

はい、実際には個々のリクエストをより高速に実行する必要があるという前提で進めます。

この場合、Webサーバーで並列コードを実行する必要があります。並列コードはASP.NETが他の要求を処理するために必要とする可能性のあるスレッドを使用するため、これは一般的には絶対に推奨されません。したがって、この決定はサーバー全体に影響を与えます。

ASP.NETで並列コードを使用する場合、Webアプリのスケーラビリティを実際に制限することを決定します。特にリクエストがまったくバースト的である場合は、かなりの量のスレッドチャーンが発生する場合もあります。同時ユーザーの数が非常に少ない(つまり、パブリックサーバーではない)ことがわかっている場合は、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.InvokeParallelコードの利点は、リクエストスレッドが並列スレッドの1つとして使用され、スレッドコンテキストで実行を再開することです(async例よりもコンテキストの切り替えが少ない)。

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

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


1
詳細な回答をありがとうございました。もう一つ明確にしたいことがあります。を使用して実行を開始するTask.Runと、コンテンツは別のスレッドで実行され、スレッドプールから取得されます。次に、ブロッキング呼び出し(サービスへRunのマイコールなど)をにラップする必要はまったくありません。メソッドの実行中にブロックされるスレッドは常に1つずつ消費されるためです。そのような状況ではasync-await、私の場合に残される唯一の利点は、一度にいくつかのアクションを実行することです。間違っていたら訂正してください。
イーデル、2014年

2
はい、Run(またはParallel)から得られる唯一の利点は同時実行性です。操作はまだスレッドをブロックしています。サービスはWebサービスであるとのことなので、Runまたはの使用はお勧めしませんParallel。代わりに、サービスの非同期APIを記述します。
スティーブンクリアリー2014年

3
@StephenCleary。「...そして各操作は非同期なので、実際にはスレッドを使用していない...」とはどういう意味ですか?
alltej 2016

3
@alltej:完全に非同期のコードの場合、スレッドはありません
Stephen Cleary 2016

4
@JoshuaFrank:直接ではありません。同期APIの背後にあるコードは、定義により、呼び出しスレッドをブロックする必要があります。のような非同期API 使用してBeginGetResponseそれらのいくつかを開始し、それらがすべて完了するまで(同期的に)ブロックすることができますが、この種のアプローチはトリッキーです-blog.stephencleary.com/2012/07/dont-block-onを参照してください-async-code.htmlおよびmsdn.microsoft.com/en-us/magazine/mt238404.aspx。通常async、可能であれば、すべての方法を採用する方が簡単でクリーンです。
Stephen Cleary

1

次のコードは、常に非同期で実行されるようにタスクを変換できることがわかりました

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

そして、私はそれを次のように使用しました

await ForceAsync(() => AsyncTaskWithNoAwaits())

これにより、任意のタスクが非同期で実行されるため、WhenAll、WhenAnyシナリオなどの用途でそれらを組み合わせることができます。

呼び出されたコードの最初の行としてTask.Yield()を追加することもできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.