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

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

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

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

スレッド プールは多数のビジー状態のスレッドを格納するのに十分な大きさにすることができるので、これは問題にならない可能性もあります。 ただし、スレッド プール内のスレッドの数は制限されています (.NET 4.5 の既定の最大値は 5,000 です)。 実行時間の長い要求のコンカレンシーが高い大規模なアプリケーションでは、使用可能なすべてのスレッドがビジー状態になる可能性があります。 この状態をスレッドの不足と呼びます。 この状態に達すると、Web サーバーによって要求がキューに置かれます。 要求キューがいっぱいになると、Web サーバーは HTTP 503 ステータス (サーバーがビジー状態です) を返して要求を拒否します。 CLR スレッド プールには、新しいスレッド インジェクションに制限があります。 コンカレンシーが急増し (つまり、Web サイトが突然大量の要求を受け取る場合)、待機時間の長いバックエンド呼び出しのために使用可能なすべての要求スレッドがビジー状態になった場合、スレッド挿入率が限られるため、アプリケーションの応答が大幅に低下する可能性があります。 さらに、スレッド プールに追加された新しい各スレッドにはオーバーヘッドがあります (1 MB のスタック メモリなど)。 同期メソッドを使用して待機時間の長い呼び出しを処理する Web アプリケーションでは、スレッド プールが .NET 4.5 の既定の最大 5,000 スレッドまで増加すると、非同期メソッドと 50 スレッドのみを使用して同じ要求を処理できるアプリケーションよりも約 5 GB 多くのメモリが消費されます。 非同期作業を行うときに、常にスレッドを使用しているとは限りません。 たとえば、非同期 Web サービス要求を行う場合、ASP.NET は async メソッド呼び出しと 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: このチュートリアルで使用するコードを含む ASP.NET MVC 4 プロジェクト。 これは、WebAPIpgw サービスへの Web API 呼び出しを行います。
  • WebAPIpgw: Products, Gizmos and Widgets コントローラーを実装する ASP.NET MVC 4 Web API プロジェクト。 WebAppAsync プロジェクトと Mvc4Async プロジェクトのデータを提供します。
  • WebAppAsync: 別のチュートリアルで使用する ASP.NET Web Forms プロジェクト。

ギズモ同期アクション メソッド

次のコードは、ギズモの一覧を表示するために使用される 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 メソッドは、ギズモ データの一覧を返す ASP.NET Web API HTTP サービスに URI を渡します。 WebAPIpgw プロジェクトには、Web API gizmos, widgetproduct コントローラーの実装が含まれています。
次の図は、サンプル プロジェクトのギズモ ビューを示しています。

Gizmos

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

このサンプルでは、新しい 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 を非同期にできるように、次の変更が適用されました。

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

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

await キーワードは、タスクが完了するまでスレッドをブロックしません。 タスクのコールバックとしてメソッドの残りの部分をサインアップし、すぐに戻ります。 待機中のタスクが最終的に完了すると、そのコールバックが呼び出され、中断したところからメソッドの実行が再開されます。 await キーワードと async キーワードと Task 名前空間の使用の詳細については、async のリファレンスを参照してください。

次のコードは、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 に対して行われた変更と似ています。

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

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

async

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

複数の操作の並列実行

非同期アクション メソッドは、アクションが複数の独立した操作を実行する必要がある場合に、同期メソッドよりも大きな利点があります。 提供されているサンプルでは、同期メソッド PWG (製品、ウィジェット、ギズモ用) に、製品、ウィジェット、ギズモの一覧を取得するための 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 パラメーターを受け取ります。 次のコードは、タイムアウトが 150 ミリ秒の GizmosCancelAsync メソッドを示しています。

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

提供されたサンプル アプリケーションで、キャンセル トークンのデモ リンクを選択すると、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.sys の拒否要求で HTTP 503 の状態が表示される場合があります。 HTTP.sys キューの制限を変更するには、次の手順を実行します。

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

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

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

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