ASP.NET MVC 4 での非同期メソッドの使用

作成者 : Rick Anderson

このチュートリアルでは、Microsoft Visual Studio の無料バージョンである Visual Studio Express 2012 for Web を使用して非同期 ASP.NET MVC Web アプリケーションを構築する基本について説明します。 Visual Studio 2012 を使用することもできます。

このチュートリアルの完全なサンプルは github で提供されています https://github.com/RickAndMSFT/Async-ASP.NET/

.NET 4.5 を組み合わせた ASP.NET MVC 4 Controller クラスを使用すると、Task<ActionResult> 型のオブジェクトを返す非同期アクション メソッドを記述できます。 .NET Framework 4 では、Task と呼ばれる非同期プログラミングの概念が導入され、MVC 4 ASP.NET Task がサポートされています。 タスクは、System.Threading.Tasks 名前空間の Task 型と関連する型で表されます。 .NET Framework 4.5 は、この非同期サポートを基にしています。await キーワードと async キーワードを使用すると、Task オブジェクトの操作が以前の非同期アプローチよりもはるかに複雑になります。 await キーワード (keyword)は、コードの一部が他のコードを非同期的に待機する必要があることを示す構文の短縮形です。 非同期キーワード (keyword)は、メソッドをタスク ベースの非同期メソッドとしてマークするために使用できるヒントを表します。 awaitasyncTask オブジェクトの組み合わせにより、.NET 4.5 で非同期コードを簡単に記述できます。 非同期メソッドの新しいモデルは、 タスク ベースの非同期パターン (TAP) と呼ばれます。 このチュートリアルでは、 await キーワードと async キーワードと Task 名前空間を使用した非同期プログラミングに関する知識があることを前提としています。

await キーワードと async キーワードの使用と Task 名前空間の詳細については、次のリファレンスを参照してください。

スレッド プールによる要求の処理方法

Web サーバーでは、.NET Frameworkは、ASP.NET 要求の処理に使用されるスレッドのプールを保持します。 要求が到着すると、その要求を処理するためにプールのスレッドがディスパッチされます。 要求が同期的に処理される場合、要求を処理するスレッドは要求の処理中にビジー状態になり、そのスレッドは別の要求を処理できません。

スレッド プールは多くのビジー 状態のスレッドに対応できる十分な大きさにできるため、これは問題ではない可能性があります。 ただし、スレッド プール内のスレッドの数は制限されています (.NET 4.5 の既定の最大値は 5,000 です)。 実行時間の長い要求のコンカレンシーが高い大規模なアプリケーションでは、使用可能なすべてのスレッドがビジー状態になる可能性があります。 この状態をスレッドの不足と呼びます。 この条件に達すると、Web サーバーは要求をキューに入れます。 要求キューがいっぱいになると、Web サーバーは HTTP 503 状態 (サーバーがビジー状態) の要求を拒否します。 CLR スレッド プールには、新しいスレッド インジェクションに関する制限があります。 コンカレンシーが急増し (つまり、Web サイトが突然多数の要求を受け取る可能性があります)、待機時間の長いバックエンド呼び出しのために使用可能なすべての要求スレッドがビジー状態になっている場合、スレッドの挿入速度が制限されると、アプリケーションの応答が非常に低くなる可能性があります。 さらに、スレッド プールに追加された新しいスレッドごとにオーバーヘッドがあります (1 MB のスタック メモリなど)。 同期メソッドを使用して待機時間の長い呼び出しを処理する Web アプリケーションでは、スレッド プールが .NET 4.5 の既定の最大 5 つまで増加し、000 スレッドは、アプリケーションが非同期メソッドを使用して同じ要求を処理できるよりも約 5 GB 多くのメモリを消費し、スレッド数は 50 個のみです。 非同期作業を行うときは、常にスレッドを使用するとは限りません。 たとえば、非同期 Web サービス要求を行う場合、ASP.NET は 非同期 メソッド呼び出しと await の間のスレッドを使用しません。 スレッド プールを使用して待機時間の長い要求を処理すると、メモリ占有領域が大きくなり、サーバー ハードウェアの使用率が低下する可能性があります。

非同期要求の処理

起動時に多数の同時要求が表示される Web アプリ、またはバースト読み込み (コンカレンシーが突然増加する) がある Web アプリでは、Web サービス呼び出しを非同期にすることで、アプリの応答性が向上します。 非同期要求の処理にかかる時間は同期要求の場合と同じです。 要求が完了するまでに 2 秒を必要とする Web サービス呼び出しを行った場合、要求は同期的または非同期的に実行されるかどうかにかかわらず、2 秒かかります。 ただし、非同期呼び出し中、スレッドは、最初の要求が完了するまで待機している間、他の要求への応答をブロックされません。 そのため、非同期要求は、実行時間の長い操作を呼び出す同時要求が多数ある場合に、要求キューとスレッド プールの増加を防ぎます。

同期または非同期のアクション メソッドの選択

ここでは、同期と非同期のアクション メソッドの使い分けに関するガイドラインを示します。 これらは単なるガイドラインです。では、各アプリケーションを個別に調べて、非同期メソッドがパフォーマンスに役立つかどうかを判断します。

一般に、次の条件に同期メソッドを使用します。

  • 操作が単純であるか短時間で完了する。
  • 効率よりも単純化の方が重要である。
  • 操作が、膨大なディスクを使用する、またはネットワーク オーバーヘッドが生じる操作ではなく、主に CPU 操作である。 CPU バインド操作で非同期アクション メソッドを使用しても利点はなく、オーバーヘッドが大きくなります。

一般に、次の条件に対して非同期メソッドを使用します。

  • 非同期メソッドを使用して使用できるサービスを呼び出していて、.NET 4.5 以降を使用しています。
  • 操作が CPU バインドではなくネットワーク バインドまたは I/O バインドである。
  • コードの単純化よりも並列化の方が重要である。
  • ユーザーが実行に時間のかかる要求を取り消すことができる機構を用意する必要がある。
  • スレッドを切り替える利点がコンテキスト切り替えのコストを上回る場合。 一般に、同期メソッドが ASP.NET 要求スレッドで待機し、何も行っていない場合は、メソッドを非同期にする必要があります。 呼び出しを非同期にすることで、ASP.NET 要求スレッドは、Web サービス要求の完了を待機している間、何の処理も停止しません。
  • テストでは、ブロック操作がサイトのパフォーマンスのボトルネックであり、これらのブロック呼び出しに非同期メソッドを使用して、IIS がより多くの要求を処理できることを示しています。

ダウンロード可能なサンプルに、非同期アクション メソッドを効果的に使用する方法を示します。 提供されるサンプルは、.NET 4.5 を使用した ASP.NET MVC 4 での非同期プログラミングの簡単なデモを提供するように設計されています。 このサンプルは、ASP.NET MVC での非同期プログラミングの参照アーキテクチャを意図したものではありません。 サンプル プログラムでは、ASP.NET Web APIメソッドが呼び出され、Task.Delay が呼び出され、実行時間の長い Web サービス呼び出しがシミュレートされます。 ほとんどの運用アプリケーションでは、非同期アクション メソッドを使用する場合にこのような明白な利点は示されません。

すべてのアクション メソッドを非同期にする必要があるアプリケーションはほとんどありません。 多くの場合、少数の同期アクション メソッドを非同期メソッドに変換すると、必要な作業量に最適な効率向上が実現します。

サンプル アプリケーション

サンプル アプリケーションは、GitHub サイトからhttps://github.com/RickAndMSFT/Async-ASP.NET/ダウンロードできます。 リポジトリは、次の 3 つのプロジェクトで構成されます。

  • Mvc4Async: このチュートリアルで使用するコードを含む MVC 4 プロジェクト ASP.NET。 WebAPIpgw サービスへの Web API 呼び出しを行います。
  • WebAPIpgw: コントローラーを実装 Products, Gizmos and Widgets する ASP.NET MVC 4 Web API プロジェクト。 WebAppAsync プロジェクトと Mvc4Async プロジェクトのデータを提供します。
  • WebAppAsync: 別のチュートリアルで使用される ASP.NET Web Forms プロジェクト。

Gizmos 同期アクション メソッド

次のコードは、ギズモの Gizmos 一覧を表示するために使用される同期アクション メソッドを示しています。 (この記事では、ギズモは架空の機械装置です)。

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

次のコードは、 GetGizmos ギズモ サービスの メソッドを示しています。

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

メソッドはGizmoService GetGizmos、Gizmos データの一覧を返す ASP.NET Web API HTTP サービスに URI を渡します。 WebAPIpgw プロジェクトには、Web API gizmos, widgetproductコントローラーの実装が含まれています。
次の図は、サンプル プロジェクトのギズモ ビューを示しています。

ギズモ

非同期ギズモ アクション メソッドの作成

このサンプルでは、新しい async キーワードと await キーワード (.NET 4.5 および Visual Studio 2012 で使用可能) を使用して、非同期プログラミングに必要な複雑な変換をコンパイラが管理できるようにします。 コンパイラでは、C# の同期制御フロー コンストラクトを使用してコードを記述できます。コンパイラは、スレッドのブロックを回避するためにコールバックを使用するために必要な変換を自動的に適用します。

次のコードは、 Gizmos 同期メソッドと非同期メソッドを GizmosAsync 示しています。 ブラウザーで HTML 5 <mark> 要素がサポートされている場合は、黄色の強調表示で GizmosAsync 変更が表示されます。

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

を非同期にできるように、次の GizmosAsync 変更が適用されました。

  • メソッドは非同期キーワード (keyword)でマークされます。これにより、本体の一部のコールバックを生成し、返される をTask<ActionResult>自動的に作成するようにコンパイラに指示されます。
  • "Async" がメソッド名に追加されました。 "Async" の追加は必須ではありませんが、非同期メソッドを記述する場合の規則です。
  • 戻り値の型が から ActionResultTask<ActionResult>変更されました。 の戻り値の Task<ActionResult> 型は進行中の作業を表し、非同期操作の完了を待機するハンドルをメソッドの呼び出し元に提供します。 この場合、呼び出し元は Web サービスです。 Task<ActionResult> は、 の結果と共に進行中の作業を表します。 ActionResult.
  • await キーワード (keyword)が Web サービス呼び出しに適用されました。
  • 非同期 Web サービス API が呼び出されました (GetGizmosAsync)。

メソッド本体の GetGizmosAsync 内部では、 GetGizmosAsync 別の非同期メソッドが呼び出されます。 GetGizmosAsync は、 Task<List<Gizmo>> データが使用可能になったときに最終的に完了する をすぐに返します。 ギズモ データを取得するまでは他の操作を行いたくないため、コードはタスクを待機します (await キーワード (keyword)を使用)。 await キーワード (keyword)は、非同期キーワード (keyword)で注釈が付けられたメソッドでのみ使用できます。

await キーワード (keyword)は、タスクが完了するまでスレッドをブロックしません。 タスクのコールバックとしてメソッドの残りの部分をサインアップし、直ちに を返します。 待機中のタスクが最終的に完了すると、そのコールバックが呼び出され、中断した場所でメソッドの実行が再開されます。 await キーワードと async キーワードと Task 名前空間の使用の詳細については、非同期参照を参照してください。

次のコードは、GetGizmos メソッドと GetGizmosAsync メソッドを示します。

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

非同期の変更は、上記の GizmosAsync に対して行われた変更と似ています。

  • メソッドシグネチャには非同期キーワード (keyword)で注釈が付け、戻り値の型は にTask<List<Gizmo>>変更され、Async はメソッド名に追加されました。
  • 非同期 HttpClient クラスは、 WebClient クラスの代わりに使用されます。
  • await キーワード (keyword)が HttpClient 非同期メソッドに適用されました。

次の図は、非同期ギズモ ビューを示しています。

非同期

ギズモ データのブラウザー表示は、同期呼び出しによって作成されたビューと同じです。 唯一の違いは、負荷が高い場合に非同期バージョンのパフォーマンスが向上する可能性がある点です。

複数の操作の並列実行

非同期アクション メソッドは、アクションが複数の独立した操作を実行する必要がある場合に、同期メソッドよりも大きな利点があります。 提供されるサンプルでは、同期メソッド PWG(Products、Widgets、Gizmos の場合) に、製品、ウィジェット、ギズモの一覧を取得するための 3 つの Web サービス呼び出しの結果が表示されます。 これらのサービスを提供する ASP.NET Web API プロジェクトでは、Task.Delay を使用して待機時間または低速のネットワーク呼び出しをシミュレートします。 遅延が 500 ミリ秒に設定されている場合、非同期 PWGasync メソッドの完了には 500 ミリ秒以上かかりますが、同期 PWG バージョンは 1,500 ミリ秒を超える時間がかかります。 同期 PWG メソッドを次のコードに示します。

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

非同期 PWGasync メソッドを次のコードに示します。

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

次の図は、 PWGasync メソッドから返されるビューを示しています。

pwgAsync

キャンセル トークンの使用

を返すTask<ActionResult>非同期アクション メソッドは取り消し可能です。つまり、AsyncTimeout 属性が指定されている場合は CancellationToken パラメーターを受け取ります。 次のコードは、 GizmosCancelAsync タイムアウトが 150 ミリ秒の メソッドを示しています。

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

次のコードは、 CancellationToken パラメーターを受け取る GetGizmosAsync オーバーロードを示しています。

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

提供されたサンプル アプリケーションで、 Cancellation Token Demo リンクを選択すると、 メソッドが GizmosCancelAsync 呼び出され、非同期呼び出しの取り消しが示されます。

高コンカレンシー/待機時間の長い Web サービス呼び出しのサーバー構成

非同期 Web アプリケーションの利点を実現するには、既定のサーバー構成にいくつかの変更を加える必要がある場合があります。 非同期 Web アプリケーションを構成してテストする場合は、次の点に注意してください。

  • Windows 7、Windows Vista、およびすべての Windows クライアント オペレーティング システムには、最大 10 個の同時要求があります。 高負荷の非同期メソッドの利点を確認するには、Windows Server オペレーティング システムが必要です。

  • 管理者特権でのコマンド プロンプトから IIS に .NET 4.5 を登録します。
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    「ASP.NET IIS 登録ツール (Aspnet_regiis.exe)」を参照してください。

  • HTTP.sys キューの制限を既定値の 1,000 から 5,000 に増やす必要がある場合があります。 設定が低すぎると、HTTP 503 状態 の要求HTTP.sys 拒否する場合があります。 HTTP.sys キューの制限を変更するには:

    • IIS マネージャーを開き、[アプリケーション プール] ウィンドウに移動します。
    • ターゲット アプリケーション プールを右クリックし、[ 詳細設定] を選択します。
      詳細
    • [ 詳細設定] ダイアログ ボックスで、 キューの長さを 1,000 から 5,000 に変更します。
      キューの長さ

    上記の画像では、アプリケーション プールで .NET 4.5 が使用されている場合でも、.NET Framework は v4.0 として一覧表示されています。 この不一致を理解するには、次を参照してください。

  • アプリケーションで Web サービスまたは System.NET を使用して HTTP 経由でバックエンドと通信する場合は、 connectionManagement/maxconnection 要素を増やす必要がある場合があります。 ASP.NET アプリケーションの場合、これは autoConfig 機能によって CPU の数の 12 倍に制限されます。 つまり、quad-proc では、IP エンドポイントへの最大 12 * 4 = 48 の同時接続を使用できます。 これは autoConfig に関連付けられているため、ASP.NET アプリケーションを増やすmaxconnection最も簡単な方法は、global.asax ファイルの from Application_Start メソッドで System.Net.ServicePointManager.DefaultConnectionLimit をプログラムで設定することです。 例については、サンプルのダウンロードを参照してください。

  • .NET 4.5 では、 MaxConcurrentRequestsPerCPU の既定値の 5000 は問題ありません。