2009年4月4日土曜日

もう一度AsyncController

正式にドキュメントが公開されてるので、改めてドキュメントに書かれてるようにサンプルを書いてみました。

ASP.NET - Release: ASP.NET MVC v1.0 Source

現時点で”Using the AsyncController.docx”はあんまり人気無いみたいね...。ダウンロード数の桁が違う。

async

ドキュメントから勝手に引用。ようするに非同期ハンドラなんですが、いろいろ(アクションだけじゃなくて非同期で大丈夫なように)とあるようで結構なコード量での対応になってます。

非同期にするにあたり、ルーティングの登録が通常の物と違います。非同期ハンドラを使わないとダメなので、MapRouteではなくMapAsyncRouteでルーティング登録。とりあえず、Futuresのアセンブリ参照とMicrosoft.Web.Mvcのusingは忘れずに。

続いて、非同期化したいコントローラのベースクラスをControllerからAsyncControllerに変更しましょう。AsyncManagerやActionInvokerなんかがあるし。ここまでは決まり事です。

非同期アクションを実装する場合、何パターンかあります。ドキュメントを上から見ていくと以下のようなパターンが書かれてます。

  • IAsyncResultパターン
  • イベントパターン
  • デリゲートパターン

それぞれ書き方も違うので一つずつ見ていきます。画像のURLを渡して、そのバイナリを取得し、そのままレスポンスするというものをサンプルに書いてみます。

まずはIAsyncResult。これはアクションが呼び出される際に、アクション内で使用する引数の他にAsyncCallbackとobjectが渡されて来ます。"IAsyncResult を使用した非同期メソッドの呼び出し"これの一番うえのパターンでしょう。End~は勝手に呼び出してくれます。

    public IAsyncResult BeginLoad(string url, AsyncCallback cb, object state)
   {
     var req = WebRequest.Create(url);
     req.Method = "get";
     return req.BeginGetResponse(cb, req);
   }

   public ActionResult EndLoad(IAsyncResult ar)
   {
     WebRequest req = ar.AsyncState as WebRequest;
     var res = req.GetResponse();
     return new FileStreamResult(res.GetResponseStream(), res.ContentType);
   }
こんな感じでしょうか。時間のかかる処理向けというよりは、Begin/Endの組み合わせでIAsyncResultを返す非同期機能を呼び出すときに使うものですね。

次にイベントパターン。最初に呼び出されるアクションの戻り値はvoidにして置いて、引数も必要なものだけを定義。完了時に呼び出されるコールバックのサフィックスにCompletedをつけるルールる、そのコールバックがActionResultを返します。ここでAsyncManagerが登場します。AsyncManager.Parametersディクショナリにコールバックに渡される引数名と同じキーで値を入れておくと、勝手に引数に渡してくれます。コールバックがいつ呼び出されるのかというとAsyncManager.OutstandingOperations.Incrementした値がDecrementで0になったときみたいです。なので、複数の時間のかかる処理を平行して実行させるとき、それぞれの処理をQueueUserWorkItemに登録。同じ数だけインクリメントをしておくけど、処理終了時点でデクリメント。すべてのQueue内の処理が完了するとカウンタが0になるのでコールバックが呼ばれる流れですね。コールバックにはサフィックスとしてCompletedをアクション名につけたものを登録する掟です。

    public void LoadE1(string url)
   {
     AsyncManager.OutstandingOperations.Increment();
     ThreadPool.QueueUserWorkItem(w => {
       var req = WebRequest.Create(url);
       req.Method = "get";
       var res = req.GetResponse();
       AsyncManager.Parameters["stream"] = res.GetResponseStream();
       AsyncManager.Parameters["contentType"] = res.ContentType;

       AsyncManager.OutstandingOperations.Decrement();
     }, null);
   }

   public ActionResult LoadE1Completed(Stream stream, string contentType)
   {
     return new FileStreamResult(stream, contentType);
   }

こんな感じで動いてます。長時間の処理でIAsyncResultじゃない場合にはこれがいいかと。でも正直AsyncManager.Parametersに値を入れるのがこのタイミングでいいのかは自身ないです。Futuresのソース見れば分かると思うけど...。

同じくイベントパターンですが、今度はAsyncManager.RegisterTaskを使って実行したい処理を登録しておく方法です。これはIAsyncResultパターンとのコンビネーションみたいな感じです。ただし自分でインクリメントやデクリメントして処理数を管理しないです。RegisterTaskの2個目の引数に渡すendDelegateが完了したら勝手にカウンタを管理してCompletedを呼び出してくれる感じです。これも複数タスクを登録可能。

    public void LoadE2(string url)
   {
     var req = WebRequest.Create(url);
     req.Method = "get";
     AsyncManager.RegisterTask(
         callback => req.BeginGetResponse(callback,null),
         ar => {
           var res = req.EndGetResponse(ar);

           AsyncManager.Parameters["stream"] = res.GetResponseStream();
           AsyncManager.Parameters["contentType"] = res.ContentType;
         });
   }

   public ActionResult LoadE2Completed(Stream stream, string contentType)
   {
     return new FileStreamResult(stream, contentType);
   }

これで。

最後がデリゲートパターン。さっきもデリゲートじゃないか!と言われればごもっとも。何がデリゲートかというとアクションの戻り値がデリゲートです。これまでとは少し感じが違って1つのアクション(メソッド)しか定義しません。コールバックはどうするかというと、コールバックをこのアクションの戻り値にしてしまうというのが、デリゲートパターンの名前の由来(ウソです、知りません)。

    public Func<ActionResult> LoadD(string url)
   {
     var req = WebRequest.Create(url);
     WebResponse res = null;
     req.Method = "get";
     AsyncManager.RegisterTask(
         callback => req.BeginGetResponse(callback, null),
         ar => {
           res = req.EndGetResponse(ar);
         });

     return () => {
       return new FileStreamResult(res.GetResponseStream(), res.ContentType);
     };
   }

1つの定義で済むので完結に書けていいですね。これもRegisterTaskを使ってるので、複数登録してもすべてが終わるまでコールバックは呼ばれないですね。

だいたいこんな感じです。

通常のControllerとAsyncControllerを簡単に比べるためのアクションを書いてみます。3秒スリープして画像ファイルをレスポンスするという簡単なものです。

同期版

    public ActionResult Dog()
   {
     Thread.Sleep(3000);
     return File("/Content/dog.jpg", "image/jpeg");
   }

非同期版

    public void Dog()
   {
     AsyncManager.OutstandingOperations.Increment();
     ThreadPool.QueueUserWorkItem(w => {
       Thread.Sleep(3000);
       AsyncManager.Parameters["path"] = "/Content/dog.jpg";
       AsyncManager.Parameters["contentType"] = "image/jpeg";
       AsyncManager.OutstandingOperations.Decrement();
     }, null);
   }

   public ActionResult DogCompleted(string path, string contentType)
   {
     return File(path, contentType);
   }

実行するとどっちも3秒たってから画像が表示されます。

async2

前回同様Measure-CommandとTinyGetでパフォーマンスを測定。

PS C:\Program Files\IIS Resources\TinyGet>  Measure-Command {.\tinyget -srv:localhost
-r:27721 -uri:/Home/Dog -threads:50 -loop:1}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 10
Milliseconds      : 165
Ticks             : 101652219
TotalDays         : 0.00011765303125
TotalHours        : 0.00282367275
TotalMinutes      : 0.169420365
TotalSeconds      : 10.1652219
TotalMilliseconds : 10165.2219

PS C:\Program Files\IIS Resources\TinyGet> Measure-Command {.\tinyget -srv:localhost
-r:27721 -uri:/Async/Dog -threads:50 -loop:1}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 7
Milliseconds      : 314
Ticks             : 73144834
TotalDays         : 8.46583726851852E-05
TotalHours        : 0.00203180094444444
TotalMinutes      : 0.121908056666667
TotalSeconds      : 7.3144834
TotalMilliseconds : 7314.4834

気持ちだね...。サンプルが良くないから。 早くするというよりも、サーバーのコネクションを素早く解放して同時実効性能を上げるのが目的なんだから早さ見てもしょうがないですよね。

今回登場してもらったわんちゃんは"SPOILED-ROTTEN-TO-THE-CORE dog on Flickr - Photo Sharing!"です。