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

作成者 : Rick Anderson

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

このチュートリアルの完全なサンプルは、
https://github.com/RickAndMSFT/Async-ASP.NET/GitHub サイトで。

4.5 Web ページ ASP.NET .NET 4.5 を組み合わせて使用すると、 Task 型のオブジェクトを返す非同期メソッドを登録できます。 .NET Framework 4 では、Task と呼ばれる非同期プログラミングの概念が導入され、ASP.NET 4.5 では 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 サービス呼び出しを非同期にすると、アプリケーションの応答性が向上します。 非同期要求の処理にかかる時間は同期要求の場合と同じです。 たとえば、要求が完了するまでに 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: 別のチュートリアルで使用されるコードを含む MVC 4 プロジェクト ASP.NET。 WebAPIpwg サービスに対して Web API 呼び出しを行います。

ギズモ同期ページ

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

Web API コントローラーに入力された、対応する詳細を含むギズモのテーブルを示す [Gizmos の同期] Web ブラウザー ページのスクリーンショット。

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

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

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

メソッド本体の内部で GetGizmosSvcAsyncGetGizmosAsync 別の非同期メソッドが呼び出されます。 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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

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

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

Web API コントローラーに入力された対応する詳細を含むギズモのテーブルを示す Gizmos Async Web ブラウザー ページのスクリーンショット。

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

RegisterAsyncTask Notes

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

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

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

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

複数の操作の並列実行

非同期メソッドは、アクションが複数の独立した操作を実行する必要がある場合に、同期メソッドよりも大きな利点があります。 提供されているサンプルでは、同期ページ PWG.aspx (Products、Widgets、Gizmos の場合) に、製品、ウィジェット、ギズモの一覧を取得するための 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 ページから返されたビューを示しています。

ウィジェット、製品、ギズモの各テーブルを示す非同期ウィジェット、製品、およびギズモ Web ブラウザー ページのスクリーンショット。

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

返されるTask非同期メソッドは取り消し可能です。つまり、Page ディレクティブの 属性が指定AsyncTimeoutされている場合は CancellationToken パラメーターを受け取ります。 次のコードは、タイムアウトが 2 番目の 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、Window 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 503 状態 の要求HTTP.sys 拒否する場合があります。 HTTP.sys キューの制限を変更するには:

    • IIS マネージャーを開き、[アプリケーション プール] ウィンドウに移動します。
    • ターゲット アプリケーション プールを右クリックし、[ 詳細設定] を選択します。
      [詳細設定] メニューが赤い四角形で強調表示されているインターネット インフォメーション サービス マネージャーのスクリーンショット。
    • [ 詳細設定] ダイアログ ボックスで、 キューの長さを 1,000 から 5,000 に変更します。
      [キューの長さ] フィールドが 1000 に設定され、赤い四角形で強調表示されている [詳細設定] ダイアログ ボックスのスクリーンショット。

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

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

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

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

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

共同作成者