Task (TAP) と APM のプログラミング・パターンの変換 (C#)


環境 : .NET Framework 4.5 (Visual Studio 2012)

こんにちは。

Build Insider OFFLINE のセッション「.NET Web プログラミングにおける非同期 I/O のすべて」にご参加いただいた皆様、ありがとうございました。
あまりに人気のないセッションで、係の人に椅子までもっていかれてしまう始末でしたが、少数でも熱心に聞いていただける方が居る限り、こうした話を続けていきたいと思います。(なお、あの椅子たちは、すべて、しばやんのセッション ルームに行ったらしいです。)

セッションでご紹介できなかったデモが多数ありましたので、順次紹介していきたいと思います。会場で、「最近、松崎さんはブログを書いていないですね」と言われてしまったので、まとめて書こうかと思いましたが、2 回くらいに分けて書きたいと思います。

なお、セッションで説明したポイント (大きな視点での考え方、問題を回避するためのべし・べからず、など) は本ブログでは記載しませんので、公開されるスライドをご参照ください。(セッション スライドは、基本的には、Slideshare に公開され、Build Insider OFFLINE のサイト からリンクされる予定です。もちろん、登壇者の方が OK された場合の話ですが。。。)

今回は、まず、APM (Begin… / End…) と TAP (Task) の変換の話 (サンプル・コード) を記載します。

 

APM から TAP への変換

このコードは簡単に紹介しましたが、まず、APM から TAP に変換する話です。

例えば、SDK を使った Windows Azure Storage へのアクセス (CloudTable) など、一部 (いや、真剣に数えると、まだまだありますね) の API は、TAP (Task) に対応しておらず、APM のメソッドのみが提供されています。
これらを、最新の ASP.NET MVC など Task ベースの .NET Web フレームワークで使用するためには、Task.Factory.FromAsync を使用して Task ベース (TAP) に変換します。(セッションでご説明した通り、TAP のフレームワークでは、TAP を使ったリソース・アクセスのほうがマッチします。) この手法は、ある意味、”常套手段” ですので、おぼえておいてください。

追記 (2013/07) : Windows Azure Storage Client Library 2.1 で、Task ベース (TAP) の Async API が提供されることになりました。詳細は、Windows Azure Storage Team Blog を参照してください。

例えば、Service reference proxy (VS の [Add Service Reference] メニューで構築した proxy) を使った WCF Data Services へのアクセスでは、現在でも、APM の API (BeginExecute, EndExecute) しか提供されていません。
これを下記の通り、Task の拡張メソッドとしてラッピングし、ASP.NET MVC の非同期メソッドで使用できます。(Task.Factory.FromAsync を使用して TAP に変換します。)

. . .
using System.Threading;
using System.Threading.Tasks;
using Sample.ServiceReference1;
. . .

public async Task<ActionResult> Index()
{
  string result = string.Empty;
  DemoEntities ctx = new DemoEntities(
    new Uri("http://astoriademoserver.cloudapp.net/HeavyService10.svc"));
  var q = ctx.CreateQuery<TestOrder>("GetOrderDelay10");
  var orders = await q.ExecuteAsync<TestOrder>();
  foreach (var item in orders)
  {
    result += item.Name + ";";
  }

  ViewBag.Result = result;
  return View();
}
. . .

static class MyExtenion
{
  public static async Task<IEnumerable<T>> ExecuteAsync<T>(
    this DataServiceQuery<T> query)
  {
    return await Task.Factory.FromAsync<IEnumerable<T>>(
      query.BeginExecute,
      query.EndExecute,
      null);
  }
}

 

TAP から APM への変換

Task の時代に入った今ではほとんど必要ありませんが、まだ稀に、APM への変換が必要な場合があります。

例えば、ASP.NET 2.0 で導入された Page.AddOnPreRenderCompleteAsync は、PageAsyncTask (Page.RegisterAsyncTask で使用) と異なり、.NET 4.5 の時代になっても Task ベースのメンバーはサポートしていません。(APM で記述する必要があります。)
このため、Task を使った処理 (リソース・アクセス) と組み合わせる場合、わざわざ APM に戻して使う必要があります。

Task を使って APM パターンにする場合は、下記の通り記述します。(下記は、HttpClient で取得した HTTP Response の Header 情報をページに表示するサンプルです。)
この変換方法は、ASP.NET に限らず、他のケースでも使用できます。

AddOnPreRenderCompleteAsync は、コールバック関数を内部で作成して BeginAsyncOperation に渡すので、ContinueWith を使って、Task 終了後にこのコールバックを呼び出します。
Task は IAsyncResult を実装しているため、下記の通り、IAsyncResult として Task そのものを渡すことができます。内部のコールバック関数が呼ばれると、その結果、下記の EndAsyncOperation が呼び出されます。EndAsyncOperation では、下記の通り、Task に Cast して (戻して)、実行結果を取得します。

. . .
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
. . .

private Task<HttpResponseMessage> reqTask;
private string pageResult = string.Empty;

protected void Page_Load(object sender, EventArgs e)
{
  if (!IsPostBack)
  {
    // if completed, do Page_PreRenderComplete
    // (write result to Page)
    this.PreRenderComplete +=
      new EventHandler(Page_PreRenderComplete);
    AddOnPreRenderCompleteAsync(
      new BeginEventHandler(BeginAsyncOperation),
      new EndEventHandler(EndAsyncOperation));
  }
}

private IAsyncResult BeginAsyncOperation(object sender,
  EventArgs e,
  AsyncCallback cb,
  object extraData)
{
  HttpClient cl = new HttpClient();
  var task = cl.GetAsync("http://heavyweb10.cloudapp.net/");
  var tcs =
    new TaskCompletionSource<HttpResponseMessage>(extraData);

  task.ContinueWith(delegate
  {
    if (task.IsFaulted)
      tcs.TrySetException(task.Exception.InnerExceptions);
    else if (task.IsCanceled)
      tcs.TrySetCanceled();
    else
      tcs.TrySetResult(task.Result);

    if (cb != null)
      cb(tcs.Task);
  },
  CancellationToken.None,
  TaskContinuationOptions.None,
  TaskScheduler.Default);

  return tcs.Task;
}

private void EndAsyncOperation(IAsyncResult ar)
{
  HttpResponseMessage msg =
    ((Task<HttpResponseMessage>)ar).Result;
  pageResult = msg.ToString(); // get response header
}

private void Page_PreRenderComplete(object sender, EventArgs e)
{
  Response.Write(pageResult);
}
. . .

また、ASP.NET の場合には、上記の方法以外に、Task (TAP) から変換するための便利なヘルパーも提供されています。
上記と同等の処理をこのヘルパー・オブジェクト (EventHandlerTaskAsyncHelper) を使って書きなおすと下記の通りです。なお、厳密には、APM のメソッドに変換するのではなく、Page.AddOnPreRenderCompleteAsync などで扱いやすい Event Handler (BeginEventHandler, EndEventHandler) に変換します。

. . .
using System.Net.Http;
. . .

private string pageResult = string.Empty;

protected void Page_Load(object sender, EventArgs e)
{
  if (!IsPostBack)
  {
    this.PreRenderComplete +=
      new EventHandler(Page_PreRenderComplete);
    var taHelper = new EventHandlerTaskAsyncHelper(
      async (s1, e1) =>
      {
        HttpClient cl = new HttpClient();
        HttpResponseMessage res =
          await cl.GetAsync("http://heavyweb10.cloudapp.net/");
        pageResult = res.ToString();
      });
    AddOnPreRenderCompleteAsync(
      taHelper.BeginEventHandler,
      taHelper.EndEventHandler);
  }
}

private void Page_PreRenderComplete(object sender, EventArgs e)
{
  Response.Write(pageResult);
}
. . .

 

キーノートのデモで使用したお玉や皿の小道具は、実は、すべて家の備品です。家内に、「皿が減った」などと言われながらも、頑張ってデモ (寸劇) を作ったんですが、そのデモに失敗するという残念な結果に終わってしまいました。。。(あれは VS が遅かったのではなく、ネットワーク接続のトラブルでした。。。)
日曜は、すべて忘れて “さくらんぼ狩り” に行ってきました。。。

 

Comments (0)

Skip to main content