none
ASP.NET 2.0 HttpHandler内で非同期処理を行いたい RRS feed

  • 質問

  • nogu611と申します。

    Webページの表示が遅いという問題が発生したため、ASP.NETのサーバー内で非同期処理を行う必要がありました。
    以下のように実装をしてみました。
    しかし、何か問題があるように感じたので、質問させてください。

    ■環境

    Windows 2008 Server
    ASP.NET 2.0

    ■処理の流れ

    サーバー側の処理は以下のようになります。
    サーバー側はASP.NETの「HttpHandler」として実装しております。
    以下の処理順は、納期の問題で変更が難しいです。

    1.Webページからの要求を受けて「処理A」を開始
    2.「処理A」の内部で「処理B」を非同期で実行
    3.「処理A」はWebページへ制御を戻す(「処理B」はサーバー内部で処理続行中)
    4.Webページから要求を受けて、「処理C」を開始(「処理C」は「処理B」の終了を待ちます。)
    5.「処理B」終了後に「処理C」の制御がWebページに戻る。

    2の部分は以下のようなコードで実装しています
    HttpContextの中の情報(Items, Cookie)を「処理B」で利用したいのですが、ハンドラの処理とは別スレッドとなるため、HttpContextを強引にコピーしています。

                        Thread thread = new Thread(new ParameterizedThreadStart(delegate(object obj)
                        {
                            //コンテキストを引き継ぎ
                            HttpContext parentContext = ((object[])obj)[0] as HttpContext;
                            HttpContext.Current = parentContext;
    
    			//ここで時間のかかる「処理B」を行います。
    
                        }));
    
                        thread.Start(new object[] { HttpContext.Current, ~略~ });

    ■質問

    不安に思っている点は以下です。

    1.HttpContextを強引にコピーして利用するのは問題ないのでしょうか?
    2.サーバー内でThreadを生成するのはそもそもよいのでしょうか?
      それともThreadPoolを利用したほうがよいのでしょうか?(Delegate.BeginInvoke等)
      スレッドプールを利用した場合、スレッドプールの枯渇が発生しないでしょうか?

    以上の質問について回答をいただけるとありがたいです。

    2012年8月18日 8:30

すべての返信

  • 以下のページは読まれましたでしょうか?

    チュートリアル: 非同期 HTTP ハンドラーの作成
    http://msdn.microsoft.com/ja-jp/library/ms227433(v=vs.100).aspx

    非同期、HTTP Handler という言葉にのみ反応してレスしてますので、関
    係なかっらた失礼しました。

    2012年8月18日 11:28
  • 以下のページの方が、説明やサンプルコードが豊富でよさそうです。(すでにご
    承知でしたら失礼しました)

    ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション
    http://msdn.microsoft.com/ja-jp/magazine/cc163463.aspx

    ただし、nogu611 さんのやりたいことが、ある一つの要求の応答時間を短縮しよ
    うということだとすると、先に紹介したページや上のページに書いてあることは
    「やりたいこと」を実現する方法とは違うと思います。

    上に紹介したページの図1あたりの説明にあるように、一つの要求に対する応答
    時間を短縮するのが目的ではなく、全体のスループットを向上させることが目的
    です。


    上のページのサンプルコードの実行には、Microsoft が提供しているサンプルデ
    ータベースの Northwind と Pubs が必要です。環境に合わせて、web.config の
    接続文字列の変更も必要です。

    また、TerraService の Web サービスの URL の変更があったためか、プロキシ
    (App_Code/TerraService.cs) が動かないので、Visual Studio で[Web 参照の
    追加(E)]で http://msrmaps.com/TerraService2.asmx を直接参照し、それに
    合わせて http ハンドラのコードを若干修正する必要があります。

    2012年8月19日 6:32
  • SurferOnWww様 回答ありがとうございます。

    返信が遅くなってしまい、申し訳ありません。

    >ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション

    >http://msdn.microsoft.com/ja-jp/magazine/cc163463.aspx

    こちらのページは拝見しておりましたが、おっしゃるように やりたいこと と少し違うように思っております。

    >チュートリアル: 非同期 HTTP ハンドラーの作成
    >http://msdn.microsoft.com/ja-jp/library/ms227433(v=vs.100).aspx

    こちらのページは拝見しておりませんでした。このページのサンプルソースがThreadPoolを利用しているようなので検証してみたいと思います。

    上記ページでは、IHttpAsyncHandler + ThreadPoolを使っているようですが、今回は、都合によりIHttpHandlerを利用したいので、IHttpHandler+ThreadPoolでASP.NETスレッドプールが枯渇したりしないかどうか確認してみようと思います。(ASP.NETスレッドプール と ThreadPoolは同じスレッドプールなのか私はよくわかっていませんので・・・)

    2012年8月20日 1:50
  • SurferOnWww様 HttpHandlerについて検証してみました。

    >ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション
    >http://msdn.microsoft.com/ja-jp/magazine/cc163463.aspx

    上記サンプルは、データベースが準備できない都合上自分で同様のコードを作ってみました。

    ASP.NETスレッドプールに影響があるかどうか、下記コードで検証を行ってみました。

    machine.configを以下のように変更して、影響がわかりやすいようにして確認しました。

    <processModel maxWorkerThreads = "1">

    結果

    ・IHttpHandler

    Thread・・・ASP.NETのスレッドプールを利用しない。

    ThreadPool・・・ASP.NETと同じスレッドプールを利用するようです。

    ・IHttpAsyncHandler

    Thread・・・ASP.NETのスレッドプールを利用しない。

    ThreadPool・・・ASP.NETと同じスレッドプールを利用するようです。

    ※IHttpAsyncHandlerについては、サーバー内での非同期処理を待つようなしくみのようで、私の意図するところとは違うようです。

    ASP.NET用のスレッドプールと、その他スレッドプールというものが存在するのかどうかわかりませんが、ThreadPoolを利用すると、サーバー全体のスループットに影響がありそうです。なので今回はThreadを利用しようかと思います。

    また、Threadを利用した場合も、ThreadPoolを利用した場合にもはやり、 HttpContext.Current は引き継がれないようです。

    クライアントに処理が戻った後もHttpContextを使い続けるのはよくない気がしております。これについてどなたか情報をお持ちではないでしょうか?

    根本的な解決には、まずシステムのデザインを再検討したほうがよさそうな気がしております。

    ---------------------------------------------------------------------

    検証に利用した同期HttpHandlerコード

    //#define THREAD
    //#define THREADPOOL
    #define DELEGATE
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Web;
    using System.Threading;
    
    namespace TestHandlerNameSpace
    {
        public class TestHandler1 : IHttpHandler
        {
    
            #region IHttpHandler メンバ
    
            public bool IsReusable
            {
                get { return false; }
            }
    
            public void ProcessRequest(HttpContext context)
            {
                //■10秒待機してASP.NETスレッドプールを浪費→キューが小さくて要求が多いとエラーになる。キューを大きくすると問題ない
                //Thread.Sleep(10000);//10秒まち。
    
    #if THREAD
                //■スレッドを作って10秒まち→何事もなかったかのように戻る。HttpContextは引き継がれない
                Thread thread = new Thread((ParameterizedThreadStart)delegate(object param)
                {
                    Thread.Sleep(10000);
                });
                thread.Start(null);
    #endif
    #if THREADPOOL
                //■スレッドプールを作って10秒まち→要求と同一コンテキストで待つのと同じ遅延が発生する。HttpContextは引き継がれない
                ThreadPool.QueueUserWorkItem((WaitCallback)delegate(object obj)
                {
                    Thread.Sleep(10000);
                }, null);
    #endif
    #if DELEGATE
                //■一応デリゲートでも試す。→スレッドプールで待つよりも処理は早い。HttpContextは引き継がれない
                ThreadStart start = (ThreadStart)delegate(){
                    HttpApplication a = new HttpApplication();
                    Thread.Sleep(10000);
                };
    
                start.BeginInvoke((AsyncCallback)delegate(IAsyncResult iar)
                {
                    ThreadStart s = iar.AsyncState as ThreadStart;
                    s.EndInvoke(iar);
                }, start);
    #endif
            }
    
            #endregion
        }
    }
    

    検証に利用した非同期HttpHandlerコード

    //#define THREAD
    //#define THREADPOOL
    #define DELEGATE
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Web;
    using System.Threading;
    using System.Collections;
    
    namespace TestHandlerNameSpace
    {
    
        public class TestHandler2 : IHttpAsyncHandler
        {
    
            #region IHttpAsyncHandler メンバ
    
            public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
            {
                AsynchOperation asynch = new AsynchOperation(cb, context, extraData);
                asynch.StartAsyncWork();
                return asynch;
            }
    
            public void EndProcessRequest(IAsyncResult result)
            {
            }
    
            #endregion
    
            #region IHttpHandler メンバ
    
            public bool IsReusable
            {
                get { return false; }
            }
    
            public void ProcessRequest(HttpContext context)
            {
                throw new Exception("The method or operation is not implemented.");
            }
    
            #endregion
    
            class AsynchOperation : IAsyncResult
            {
                private bool _completed;
                private Object _state;
                private AsyncCallback _callback;
                private HttpContext _context;
    
                bool IAsyncResult.IsCompleted { get { return _completed; } }
                WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
                Object IAsyncResult.AsyncState { get { return _state; } }
                bool IAsyncResult.CompletedSynchronously { get { return false; } }
    
                public AsynchOperation(AsyncCallback callback, HttpContext context, Object state)
                {
                    _callback = callback;
                    _context = context;
                    _state = state;
                    _completed = false;
                }
    
                public void StartAsyncWork()
                {
                    //■10秒待機してASP.NETスレッドプールを浪費→キューが小さくて要求が多いとエラーになる。キューを大きくすると問題ない
                    //Thread.Sleep(10000);//10秒まち。
    
    #if THREAD
                    //■スレッドを作って10秒まち→何事もなかったかのように戻る。HttpContextは引き継がれない
                    Thread thread = new Thread((ParameterizedThreadStart)StartAsyncTask);
                    thread.Start(null);
    #endif
    #if THREADPOOL
                    //■スレッドプールを作って10秒まち→要求と同一コンテキストで待つのと同じ遅延が発生する。HttpContextは引き継がれない
                    ThreadPool.QueueUserWorkItem((WaitCallback)StartAsyncTask, null);
    #endif
    #if DELEGATE
                    //■一応デリゲートでも試す。→スレッドプールで待つよりも処理は早い。HttpContextは引き継がれない
                    ParameterizedThreadStart start = (ParameterizedThreadStart)StartAsyncTask;
    
                    start.BeginInvoke(null, null, start);
    #endif
                    //ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask), null);
                }
    
                private void StartAsyncTask(Object workItemState)
                {
                    Thread.Sleep(10000);
                    _completed = true;
                    _callback(this);
                }
            }
    
        }
    
    }
    

    2012年8月20日 6:49
  • サーバー側の処理は以下のようになります。
    サーバー側はASP.NETの「HttpHandler」として実装しております。
    以下の処理順は、納期の問題で変更が難しいです。

    1.Webページからの要求を受けて「処理A」を開始
    2.「処理A」の内部で「処理B」を非同期で実行
    3.「処理A」はWebページへ制御を戻す(「処理B」はサーバー内部で処理続行中)
    4.Webページから要求を受けて、「処理C」を開始(「処理C」は「処理B」の終了を待ちます。)
    5.「処理B」終了後に「処理C」の制御がWebページに戻る。

    フォーラムには納期の問題は関係ありませんから…。1.と4.は別ページですか? 同一ページなら非同期にしたところで何も変わらないと思います。

    不安に思っている点は以下です。

    1.HttpContextを強引にコピーして利用するのは問題ないのでしょうか?
    2.サーバー内でThreadを生成するのはそもそもよいのでしょうか?
      それともThreadPoolを利用したほうがよいのでしょうか?(Delegate.BeginInvoke等)
      スレッドプールを利用した場合、スレッドプールの枯渇が発生しないでしょうか?

    HttpContextはthread local storageだったように思います。コピーしてどうなるか把握できているのなら構いませんが、別スレッドになっている処理BでCookie書き換えとかされている場合は意図通りに動作しないかも。必要なデータのみを渡すのがあるべき姿です。

    リクエスト数に比例して動作するのなら、ThreadであれThreadPoolであれDoS攻撃を受けた時点でメモリ不足などでサービス不能になりますね。

    2012年8月20日 14:33
  • 佐祐理様

    返信ありがとうございます。

    >フォーラムには納期の問題は関係ありませんから…。

    >1.と4.は別ページですか? 同一ページなら非同期にしたところで何も変わらないと思います。

    申し訳ありません、肝心なところの説明が不足しておりました。1 と4は別ページになります。1のページから4のページへのページ遷移が遅いということで、もともと一つだった、処理Aと処理Bを分離して時間がかかる処理Bを非同期にしてはどうかということでこの対応をするに至りました。

    たしかに、フォーラムには納期の問題は関係ないですね・・・私個人の事情ですね・・・。

    >HttpContextはthread local storageだったように思います。コピーしてどうなるか把握できているのなら構いませんが、別スレッドになっている処理BでCookie書き換えとかされている場合は意図通りに動作しないかも。必要なデータのみを渡すのがあるべき姿です。

    HttpContextはTLSにあるようですが、マネージオブジェクトなので、参照していれば レスポンス送信後(スレッド終了後)もGCされることはないでしょう・・・ということでコピーしております。レスポンス送信後にCookieやResponseを書換えてもWebページには送信されないという点はケアできてると思います。(本件とは関係ないかもしれないですが、ThreadPoolのTLSってどういう扱いになるのかという疑問がわいてきました)

    たしかに必要なデータのみ渡すのがわかりやすくて、あるべき姿だと思います。しかし、これも私的な事情ではありますが、プログラムで共通利用している部品が内部でHttpContextを直接参照しており、そこがどうしても変更不能だったので苦肉の策としてHttpContextをコピーしています。

    >リクエスト数に比例して動作するのなら、ThreadであれThreadPoolであれDoS攻撃を受けた時点でメモリ不足などでサービス不能になりますね。

    Webページは見積を作成するものなのですが、この非同期処理は、見積開始毎に1回走る処理です。リクエスト数に比例というわけでもないので↑の私の検証はあまり意味がなかったかもしれません。Webサイトの利用頻度は高いのですが、コンシューマー向けのでなくビジネス向けのものなので、Dos攻撃については多少目をつむりたいところです・・・。

    2012年8月21日 1:11