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

作成者: Rick Anderson

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

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

ASP.NET 4.5 Web ページを .NET 4.5 と組み合わせて使用すると、Task 型のオブジェクトを返す非同期メソッドを登録できます。 .NET Framework 4 では、Task と呼ばれる非同期プログラミングの概念が導入され、ASP.NET 4.5 は 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 サービス呼び出しを非同期にすることで、アプリケーションの応答性が向上します。 非同期要求の処理にかかる時間は同期要求の場合と同じです。 たとえば、完了までに 2 秒を要する Web サービス呼び出しが要求によって行われる場合は、同期的に行われるか非同期的に行われるかに関係なく要求の処理には 2 秒かかります。 ただし、非同期呼び出しでは、スレッドが最初の要求の完了を待つ間に他の要求に応答できなくなることはありません。 そのため、非同期要求は、実行時間の長い操作を呼び出す同時要求が多数ある場合に、要求キューとスレッド プールの増加を防ぎます。

同期または非同期メソッドの選択

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

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

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

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

  • 非同期メソッドを通じて使用できるサービスを呼び出しており、.NET 4.5 以降を使用している。

  • 操作が CPU バインドではなくネットワーク バインドまたは I/O バインドである。

  • コードの単純化よりも並列化の方が重要である。

  • ユーザーが実行に時間のかかる要求を取り消すことができる機構を用意する必要がある。

  • スレッドを切り替える利点がコンテキスト切り替えのコストを上回っている。 一般に、処理を行っていない間に同期メソッドが ASP.NET 要求スレッドをブロックしている場合は、メソッドを非同期にする必要があります。 呼び出しを非同期にすると、ASP.NET 要求スレッドは、Web サービス要求の完了を待機している間に、何も処理を実行せずにブロックされることはありません。

  • テストによって、ブロック操作がサイトのパフォーマンスのボトルネックになっていること、およびこのようなブロッキング呼び出しに対して非同期メソッドを使用すると IIS で処理できる要求が増えることが示されている。

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

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

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

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

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

Gizmos 同期ページ

次のコードは、ギズモの一覧を表示するために使用される Page_Load 同期メソッドを示しています。 (この記事では、ギズモは架空の機械デバイスです)。

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

次のコードは、ギズモ サービスの 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 コントローラーの実装が含まれています。
次の図は、サンプル プロジェクトのギズモ ページを示しています。

Screenshot of the Sync Gizmos web browser page showing the the table of gizmos with corresponding details as entered into the web API controllers.

非同期ギズモ ページの作成

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

ASP.NET 非同期ページには、Async 属性が "true" に設定された Page ディレクティブが含まれている必要があります。 次のコードは、GizmosAsync.aspx ページの Async 属性が "true" に設定された Page ディレクティブを示しています。

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

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

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

非同期バージョン:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

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

  • Page ディレクティブの Async 属性が "true" に設定されている必要があります。
  • RegisterAsyncTask メソッドは、非同期的に実行されるコードを含む非同期タスクを登録するために使用されます。
  • 新しいメソッド GetGizmosSvcAsyncasync キーワードでマークされます。これは、本体の一部のコールバックを生成し、返される Task を自動的に作成するようにコンパイラに指示します。
  • 非同期メソッド名に "Async" が追加されました。 "Async" の追加は必須ではありませんが、非同期メソッドを記述する場合の慣例です。
  • 新しい GetGizmosSvcAsync メソッドの戻り値の型は Task です。 Task の戻り値の型は進行中の作業を表し、非同期操作の完了を待機するハンドルをメソッドの呼び出し元に提供します。
  • await キーワードが Web サービス呼び出しに適用されました。
  • 非同期 Web サービス API が呼び出されました (GetGizmosAsync)。

GetGizmosSvcAsync メソッド本体の内部で、別の非同期メソッド 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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

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

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

Screenshot of the Gizmos Async web browser page showing the table of gizmos with corresponding details as entered into the web API controllers.

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

RegisterAsyncTask に関するメモ

RegisterAsyncTask でフックされたメソッドは、PreRender の直後に実行されます。

次のコードに示すように、async void ページ イベントを直接使用する場合:

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

イベントが実行されるタイミングを完全には制御できなくなりました。 たとえば、.aspx と .Master の両方が Page_Load イベントを定義し、その一方または両方が非同期である場合、実行の順序を保証することはできません。 同じ不確定な順序がイベント ハンドラー (async void Button_Click など) に対して適用されます。

複数の操作の並列実行

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

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

非同期 PWGasync 分離コードを次に示します。

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

次の画像は、非同期 PWGasync.aspx ページから返されたビューを示しています。

Screenshot of the Asynchronous Widgets, Products, and Gizmos web browser page showing the Widgets, Products, and Gizmos tables.

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

Task を返す非同期メソッドは取り消し可能です。つまり、Page ディレクティブの AsyncTimeout 属性が指定されている場合は、CancellationToken パラメーターを受け取ります。 次のコードは、タイムアウトが 1 秒の GizmosCancelAsync.aspx ページを示しています。

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

次のコードは、GizmosCancelAsync.aspx.cs ファイルを示しています。

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

提供されたサンプル アプリケーションで、GizmosCancelAsync リンクを選択すると、GizmosCancelAsync.aspx ページが呼び出され、非同期呼び出しのキャンセル (タイムアウトによる) が示されます。 遅延時間はランダムな範囲内にあるため、タイムアウト エラー メッセージを取得するには、ページを数回更新することが必要になる場合があります。

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

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

  • Windows 7、Windows Vista、Windows 8、すべての 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 マネージャーを開き、[アプリケーション プール] ウィンドウに移動します。
    • ターゲット アプリケーション プールを右クリックし、[詳細設定] を選択します。
      Screenshot of the Internet Information Services Manager showing the Advanced Settings menu highlighted with a red rectangle.
    • [詳細設定] ダイアログ ボックスで、[キューの長さ] を 1,000 から 5,000 に変更します。
      Screenshot of the Advanced Settings dialog box showing the Queue Length field set to 1000 and highlighted with a red rectangle.

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

  • .NET のバージョン管理とマルチターゲット - .NET 4.5 は .NET 4.0 へのインプレース アップグレード

  • ASP.NET 2.0 ではなく 3.5 を使用するように IIS アプリケーションまたは AppPool を設定する方法

  • .NET Framework のバージョンおよび依存関係

  • アプリケーションで 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 で問題ありません。

共同作成者