WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

非同期 HTTP ハンドラ (2)

by WebSurfer 2018年3月8日 18:56

先の記事「非同期 HTTP ハンドラ」で、Web サービスに非同期でアクセスする HTTP ジェネリックハンドラのサンプルを書きました。

その HTTP ハンドラは IHttpAsyncHandler インターフェイスを継承して BeginProcessRequest, EndProcessRequest メソッドを実装するという旧来の方法を使っており、記事に書いてありますように非常に複雑なコードになります。

ここまで複雑なことをしなければならないのなら、HTTP 503 エラー (Server Too Busy) が頻発して困っているというような事情がなければ、従来通り同期版のままでもいいかなって気もします。(笑)

ですが、.NET 4.5 から簡単に非同期呼び出しが実装できるようになったそうです。どのぐらい簡単にできるのか、同じ機能を async / await を利用して実装してみました。

非同期 HTTP ハンドラ

確かにはるかに簡単でした。前のように、何がどうなっているのかを調べて、悩んで、時間をかけて実装するという手間は大幅に減っています。

上の画像は、この記事に書いた方法で作成した HTTP ハンドラを、ブラウザから直接呼び出した結果です。具体的にどのように実装したかは以下に書きます。

HelloWorldWcfService.cs

先の記事の Web サービスは WCF サービスに変更しました。HelloWorld メソッドの実装は先の記事の Web サービスのものと全く同じです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.ServiceModel.Activation;

[AspNetCompatibilityRequirements(RequirementsMode =
            AspNetCompatibilityRequirementsMode.Allowed)]
public class HelloWorldWcfService : IHelloWorldWcfService
{
    public string HelloWorld(int callDuration)
    {
        Thread.Sleep(callDuration);
        return String.Format(
          "Hello World from WcfService. Call Time: {0}",
          callDuration);
    }
}

WCF サービスに変更したのは、Web サービスが Legacy Technology だからということもありますが、一番の理由はサービス参照の追加を行うと自動生成されるサービスプロキシに含まれる非同期版メソッドが利用できるからです。

(非同期版メソッドについて、詳しくは先に記事「WCF サービスの非同期呼び出し」を見てください)

HelloWorldAsyncHnadler.ashx

Visual Studio で、既存の ASP.NET Web Forms アプリにジェネリックハンドラーを追加します。.ashx ファイルには何も手を加える必要はありません。

<%@ WebHandler Language="C#"
    CodeBehind="HelloWorldAsyncHandler.ashx.cs"
    Class="WebFormsApp.HelloWorldAsyncHandler" %>

Web アプリケーションプロジェクトの場合、.ashx ファイルとそのコードビハインドの .ashx.cs ファイルは別になります。Web サイトプロジェクトの場合、すべて .ashx ファイルに含まれるという違いがあります。

HelloWorldAsyncHandler.ahsx.cs

自動生成されたコードは同期版ハンドラのベースとなるものです。これを HttpTaskAsyncHandler クラスを継承したクラスに書き換えます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using WebFormsApp.HelloWorldServiceReference;

namespace WebFormsApp
{
    public class HelloWorldAsyncHandler : HttpTaskAsyncHandler
    {
        HelloWorldWcfServiceClient client;

        // これが呼び出されることはない。万一呼び出されたら例外
        // をスローして自爆する
        public override void ProcessRequest(HttpContext context)
        {
            throw new InvalidOperationException();
        }
        
        public override async Task ProcessRequestAsync(
                                            HttpContext context)
        {
            context.Response.Write(
                "<p>Before await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            client = new HelloWorldWcfServiceClient();

            string result = await client.HelloWorldAsync(3000);

            context.Response.Write(
                "<p>After await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            context.Response.Write("<p>" + result + "</p>");
        }

        public override bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

上のコードの WebFormsApp.HelloWorldServiceReference はサービスプロキシの名前空間です。HelloWorldWcfServiceClient コンストラクタ、HelloWorldAsync メソッドの定義は自動生成される Reference.cs ファイルにあります。

結局は先の記事「非同期 HTTP ハンドラ」と同様なことを行っていて、先の記事のコードの BeginProcessRequest メソッドが await キーワードの前に、EndProcessRequest メソッドが await の後に実行されているようです。

BeginProcessRequest, EndProcessRequest メソッドをプログラマがコードを書いて実装しなくて済むよう、コンパイラが肩代わりしているという感じです。

Tags: , ,

ASP.NET

WCF サービスの非同期呼び出し

by WebSurfer 2018年2月28日 16:47

Visual Studio 2015 Community で WCF のサービス参照を追加して生成されるサービスプロキシには非同期版のメソッドも含まれ、それを利用すれば async / await キーワードを付与することで簡単に非同期呼び出しができるという話を書きます。

WCF のクライアント

何を今さらと言われるかもしれませんが、自分としては新発見で、これは備忘録として残しておかねばと思いましたので。(笑)

上の画像は Microsoft のチュートリアル「10 行でズバリ!! [C#] WCF サービスの作成と利用」をベースに作った WPF クライアントアプリです。string 型のデータを応答として返す WCF サービスを呼び出して、その応答を表示しています。

上のチュートリアルにある通り、クライアントアプリから WCF サービスを呼び出すのに利用するサービスプロキシを生成するため、サービス参照の追加を行います。

その際、「サービス参照の追加」ダイアログで[詳細設定(V)...]をクリックすると表示される「サービス参照設定」ダイアログを見ると、Visual Studio 2015 Community ではデフォルトで以下の画像の設定になっています。

サービス参照の追加

この設定画面で[非同期操作の生成を許可する(P)]にチェックが入っていて、[タスクベースの操作を生成する(T)]が選択されているのがポイントのようです。自信度はあまり高くないですが・・���(汗)

ダイアログ右上の ? マークをクリックすると表示されるヘルプを読んでも何のことだかよく分かりませんでしたが、Microsoft のドキュメント「同期操作と非同期操作」を読むと、その中の「タスク ベースの非同期パターン」が相当するのではないかと思いました。

(Visual Studio 2010 Professional では[非同期操作の生成(G)]というのがありますが、デフォルトではチェックは入っていません。未確認ですが、.NET 4.5 以降でしか使用できない async / await キーワードを利用して使うものではなさそうです)

サービス参照の追加が完了すると、以下の画像の通り、同期版の GetData メソッドに加えて非同期版の GetDataAsync メソッドも生成されているのがインテリセンスの表示から分かります。

インテリセンスの表示

クライアントアプリからは、async / await キーワードを使って非同期版メソッドを呼び出せば、時間がかかる WCF サービスから応答が返ってくるまで UI がブロックされるということはなくなります。

それを試すには以下のようにします。

WCF サービス

まず、WCF のメソッドに 3 秒の待ち時間を入れます。これにより、同期の場合は呼び出し側の WPF アプリは 3 秒ブロックされますが、非同期の場合はブロックされないことを確認できます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;

// このサービスをホストする ASP.NET アプリの web.config
// で aspNetCompatibilityEnabled="true" と設定されている
// 場合は以下の属性が必要
[AspNetCompatibilityRequirements(RequirementsMode = 
             AspNetCompatibilityRequirementsMode.Allowed)]
public class Service : IService
{
  public string GetData(int value)
  {
    // 非同期実行を確認するため追加
    System.Threading.Thread.Sleep(3000);

    return string.Format("You entered: {0}", value);
  }

  // CompositeType は IService.cs に自動生成されたクラス
  public CompositeType 
      GetDataUsingDataContract(CompositeType composite)
  {
    // 非同期実行を確認するため追加        
    System.Threading.Thread.Sleep(3000);
        
    if (composite == null)
    {
      throw new ArgumentNullException("composite");
    }

    if (composite.BoolValue)
    {
      composite.StringValue += "Suffix";
    }

    return composite;
  }
}

WPF クライアントアプリ

呼び出し側のクライアントアプリでは以下のようにします。一番上の画像で、[同期呼び出し]ボタンのクリックイベントのハンドラが button_Click で、[非同期呼び出し]の方が button1_Click です。

// 同期
private void button_Click(object sender, RoutedEventArgs e)
{
  var client = new TestService.ServiceClient();
  var composite = new TestService.CompositeType();
  label.Content = client.GetData(12345);
  composite.BoolValue = checkBox.IsChecked == true;
  composite.StringValue = textBox.Text;
  composite = client.GetDataUsingDataContract(composite);
  label1.Content = composite.StringValue;
}

// 非同期
// async void を async Task に変更すると、メソッドとデリゲート
// 型との間に互換性が無いというエラーになる
private async void button1_Click(object sender, 
                                         RoutedEventArgs e)
{
  var client = new TestService.ServiceClient();
  var composite = new TestService.CompositeType();
  label.Content = await client.GetDataAsync(12345);
  composite.BoolValue = checkBox.IsChecked == true;
  composite.StringValue = textBox.Text;
  composite = 
    await client.GetDataUsingDataContractAsync(composite);
  label1.Content = composite.StringValue;
}

[同期呼び出し]ボタンをクリックすると上の画像のアプリは WCF サービスから応答が返ってくるまで操作できませんが、[非同期呼び出し]ボタンクリックなら操作可能で応答も期待通り返ってくることが確認できました。こんな簡単なことでホントにいいのか・・・という不安はありますが。(汗)

あと、Microsoft の文書「非同期プログラミングのベストプラクティス」には「async void メソッドよりも async Task メソッドを利用する」ということが書いてありますが、それはできなかったです(上のコードのコメントに書いたようにエラーになります)。解決方法があるのかどうかわかりませんが、分かったら追記することにします。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年12月  >>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar