2018/3/8 追記:
この記事と同じ機能を持つ非同期 HTTP ハンドラを .NET 4.5 から利用できるようになった async / await を利用して実装してみました。その記事は「
非同期 HTTP ハンドラ (2)」にありますので興味があれば見てください。この記事の方法が時代遅れなのが分かると思います。
ASP.NET の非同期プログラミングモデルには、(1) 非同期ページ、(2) 非同期 HTTP ハンドラ、(3) 非同期 HTTP モジュールの 3 つがあります。このうち、(2) 非同期 HTTP ハンドラを使って Web サービスのメソッドを非同期に呼び出す場合について、いろいろ不明な点があったので、それらを調べて分かったことを備忘録として書いておきます。
MSDN マガジンの March 2007 の記事「ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション (Wicked Code: Scalable Apps with Asynchronous Programming in ASP.NET)」が、非同期プログラミングの目的、メリット、仕組みなどをサンプルコードを使って詳しく説明しており、大変参考になりました。(2017/5/20 追記:記事は .chm ファイル形式で保存されており、MSDN Magazine Issues and Downloads からダウンロードして読むことができます)
(2015/12/29 追記:非同期プログラミングのメリット、仕組みなど基本的な説明を書いた新しい記事 ASP.NET の非同期/待機の概要を紹介しておきます。.NET 4.5 以降で利用できる async / await を使うことが前提です)
ただし、その中の非同期 HTTP ハンドラのサンプルは複雑すぎて、その仕組みなどがよく分かりませんでした。特に分からなかったのは以下の点です。
-
Web サービスのメソッドを非同期で呼び出すメソッドをどのように生成するか(例えば、Web サービスのメソッドが HelloWorld だったとすると、BeginHelloWorld と EndHelloWorld をどのように作成するか)。
-
非同期 HTTP ハンドラのコードで非同期呼び出しのためのメソッド(上の例で言うと、BeginHelloWorld メソッドと EndHelloWorld メソッド)をどのように呼び出すか。
-
BeginProcessRequest メソッドの第 2 引数として渡されるコールバックメソッドのデリゲートは何か。
-
EndProcessRequest メソッドが呼び出される仕組みとそのタイミングはどうなっているか。
という訳で、簡単なサンプルを作って上記の点について調べてみました。下の画像は、説明のためにここに紹介したサンプルを実行したときのものです。
サンプルとして、以下の Web サービスを非同期 HTTP ハンドラを使って利用することを考えます。これに定義されているのは HelloWorld メソッドだけです。同期 HTTP ハンドラではこれを呼べば済みますが、非同期 HTTP ハンドラを使う場合は非同期で HelloWorld メソッドを呼び出し、結果を処理しなければなりません。
非同期呼び出しのために、BeginHelloWorld メソッドと EndHelloWorld メソッドが必要ですが、それらはどのように作ればよいのでしょうか?
(1) Web サービス (141-HelloWorldWebService.asmx)
<%@ WebService Language="C#"
Class="_141_HelloWorldWebService" %>
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Threading;
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class _141_HelloWorldWebService : WebService {
[WebMethod]
public string HelloWorld(int callDuration)
{
Thread.Sleep(callDuration);
return String.Format(
"Hello World from WebService. Call Time: {0}",
callDuration);
}
}
答えはサービスプロキシを定義することです。と言っても、自力でコードを書く必要はなく、Visual Studio を使ってサービス参照を追加するか、または、SDK にある Wsdl.exe ツールを利用してサービスプロキシクラスを自動生成すれば OK です。ここでは、後者の Wsdl.exe ツールを利用した例を書きます。
Wsdl.exe ツールの詳しい使用方法は、MSDN ライブラリの XML Web サービス プロキシの作成 にあります。これを参考に、上記の Web サービスの URL とプロキシのコードの出力先のファイル名をパラメータとして、コマンドラインから Wsdl.exe を実行します。以下の画像のような感じです。
この結果、上記 (1) の Web サービスからプロキシクラス HelloWorldWebService.cs が自動生成されます。コードは以下のとおりです(一部省略、改行等をしています)。
(2) Web サービスプロキシ (HelloWorldWebService.cs)
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;
[System.CodeDom.Compiler.GeneratedCodeAttribute(
"wsdl", "2.0.50727.3038")]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Web.Services.WebServiceBindingAttribute(
Name="_141_HelloWorldWebServiceSoap",
Namespace="http://tempuri.org/")]
public partial class _141_HelloWorldWebService :
System.Web.Services.Protocols.SoapHttpClientProtocol
{
private System.Threading.SendOrPostCallback
HelloWorldOperationCompleted;
public _141_HelloWorldWebService()
{
this.Url =
"http://msdntestnew/141-HelloWorldWebService.asmx";
}
public event HelloWorldCompletedEventHandler
HelloWorldCompleted;
[System.Web.Services.Protocols.SoapDocumentMethodAttribute(
"http://tempuri.org/HelloWorld",
RequestNamespace="http://tempuri.org/",
ResponseNamespace="http://tempuri.org/",
Use=System.Web.Services.Description.SoapBindingUse.Literal,
ParameterStyle=
System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
public string HelloWorld(int callDuration)
{
object[] results = this.Invoke(
"HelloWorld", new object[] { callDuration });
return ((string)(results[0]));
}
public System.IAsyncResult BeginHelloWorld(
int callDuration,
System.AsyncCallback callback,
object asyncState)
{
return this.BeginInvoke(
"HelloWorld",
new object[] { callDuration },
callback,
asyncState);
}
public string EndHelloWorld(System.IAsyncResult asyncResult)
{
object[] results = this.EndInvoke(asyncResult);
return ((string)(results[0]));
}
// ・・・中略・��・
}
上に示したプロキシクラスに、SoapHttpClientProtocol.BeginInvoke メソッド と SoapHttpClientProtocol.EndInvoke メソッド をラップした BeginHelloWorld メソッドと EndHelloWorld メソッドが生成されているのが分かるでしょうか?
このファイル HelloWorldWebService.cs を、Web アプリケーションのルート直下の App_Code フォルダに置けばプロキシは使用可能になり、それに定義されている BeginHelloWorld メソッドと EndHelloWorld メソッドを利用して、以下のように非同期 HTTP ハンドラを作成できます。
(3) 非同期 HTTP ハンドラ (141-HelloWorldAsyncHandler2.ashx)
<%@ WebHandler Language="C#"
Class="_141_HelloWorldAsyncHandler2" %>
using System;
using System.Web;
using System.Threading;
public class _141_HelloWorldAsyncHandler2 : IHttpAsyncHandler
{
private HttpContext _context;
private _141_HelloWorldWebService _serviceProxy;
// これが呼び出されることはない。万一呼び出されたら例外
// をスローして自爆する
public void ProcessRequest (HttpContext context)
{
throw new InvalidOperationException();
}
public bool IsReusable
{
get { return false; }
}
// まず最初にこのメソッドが呼ばれる。
// cb には ASP.NET が内部で生成したコールバックメソッド
// void OnAsyncHandlerCompletion(System.IAsyncResult)
// のデリゲートが渡される。これが EndProcessRequest メ
// ソッドを呼び出す。extraData には null が渡される。
public IAsyncResult BeginProcessRequest(
HttpContext context,
AsyncCallback cb,
Object extraData)
{
this._context = context;
context.Response.Write(
"<p>BeginProcessRequest:<br />" +
" IsThreadPoolThread is " +
Thread.CurrentThread.IsThreadPoolThread +
"<br />" +
" ManagedThreadId is " +
Thread.CurrentThread.ManagedThreadId.ToString() +
"</p>");
_serviceProxy = new _141_HelloWorldWebService();
// 終了すると引数 cb に設定したコールバックデリゲート
// が起動される。その中で EndProcessRequest メソッドが
// 呼び出される。
return _serviceProxy.BeginHelloWorld(3000, cb, null);
}
// 引数 ar には BeginProcessRequest メソッドの戻り値であ
// る IAsyncResult オブジェクトが渡される。
public void EndProcessRequest(IAsyncResult ar)
{
_context.Response.Write(
"<p>EndProcessRequest:<br />" +
" IsThreadPoolThread is " +
Thread.CurrentThread.IsThreadPoolThread +
"<br />" +
" ManagedThreadId is " +
Thread.CurrentThread.ManagedThreadId.ToString() +
"</p>");
string result = _serviceProxy.EndHelloWorld(ar);
_context.Response.Write("<p>" + result + "</p>");
}
}
非同期 HTTP ハンドラでは、BeginProcessRequest メソッドが最初に実行されます。引数 context には現在の HttpContext オブジェクトへの参照が、cb には ASP.NET が内部で生成したコールバックメソッドのデリゲートが、extraData には null が渡されます。
BeginProcessRequest メソッドでは、プロキシクラスを初期化して BeginHelloWorld メソッドを呼び出し、HelloWorld メソッドの非同期実行を開始します。非同期実行の開始後すぐに制御が戻って、ここまでの処理に使用していたスレッドはスレッドプールに戻されます。
BeginHelloWorld メソッドの第 1 引数は HelloWorld メソッドに渡されます(ここでは、待機時間 3000 即ち 3 秒を渡しています)。第 2 引数 cb には BeginProcessRequest メソッドの引数 cb に渡されたコールバックメソッドのデリゲートがそのままコピーされます。第 3 引数は BeginProcessRequest メソッドの戻り値、即ち EndProcessRequest メソッドの引数 ar に渡される IAsyncResult オブジェクトから、AsyncState プロパティを使って取得できるデータですが、ここでは使用しないので null としています。
ここで覚えておくべきことは、BeginHelloWorld メソッドの第 2 引数として渡されたコールバックメソッドのデリゲートが実行されると EndProcessRequest メソッドが実行されることです(逆に言えば、コールバックメソッドが実行されないとフリーズしてしまう)。
非同期処理の完了後、コールバックメソッドのデリゲートが実行され、EndProcessRequest メソッドが呼び出されます。その際、スレッドプールから空いているスレッドを取得して EndProcessRequest メソッドを実行します。この時、引数 ar には BeginProcessRequest メソッドの戻り値である IAsyncResult オブジェクトが渡されます。
EndProcessRequest メソッドの中で、EndHelloWorld メソッドを実行し、Web サービスからの応答をその戻り値から取得します。EndProcessRequest メソッドの終了後、制御がコールバックメソッドに戻って全体のタスクが終了します。
上記の非同期 HTTP ハンドラを iframe の src 属性に設定した aspx ページの例は以下の通りです。一番上の画像は、以下の aspx ページの実行結果です。
(4) aspx ページ (141-HelloWorldAsyncHandler.aspx)
<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<iframe src="141-HelloWorldAsyncHandler2.ashx"
id="iframe2"
width="400px"
height="200px">
</iframe>
</form>
</body>
</html>