QA@IT

IHttpModule継承のモジュール内で非同期処理を行いたい<タイトル変更>

4453 PV

こんにちは、お世話になります。

現在、Webへのアクセスログを非同期で残そうと頑張っていますが、なかなか思うような非同期処理が実現できずに困っております。
WebのResponseを端末側に返し終わるのとは別にアクセスログを保存したいのですが、アクセスログの保存が終るのを待ってから
Responseの送信が終るか、アクセスログが残らないと言うケースが発生しています。

例えば、下記IHttpModuleの例ではSetlogが動きません。

public void Init(HttpApplication context)
{
    context.LogRequest += new EventHandler(LogRequestEvent);
}
public void LogRequestEvent(object sender, EventArgs e)
{
    int i = 0;
    ThreadPool.QueueUserWorkItem(new WaitCallback(SetLog), i);
}

private void SetLog(object state)
{
    int i = (int)state;
    // データーベースにアクセスログを登録
}

上記の例で言うと、SetLogをWebのResponse送信に影響を与えずに非同期で実行させたいのです。
皆さん、解決方法や問題点などを教えて頂けないでしょうか。

以上、宜しくお願い致します。
 
 
2015-06-18 21:35 実際にやりたい事とタイトルがズレている事がわかったため、タイトルを変更致しました。

  • ええと、とりあえずは私が書いたやり方で正確に作ってみてください。
    作成するコレクションの種類、作成タイミングはInitではなくstaticコンストラクタかフィールドの宣言と同時で、スレッド作成もInitではなくstaticコンストラクタ、ループはきちんとGetCosumingEnumerableをforeachてループ、こんなところですかね。
    -

回答

転記ミスがあるかもしれませんが、だいたいこんな感じです。

using System;
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Threading;
using System.Web;

namespace HttpModule
{
    public class AsyncLogModule : IHttpModule
    {
        private static BlockingCollection<string> s_queue =
            new BlockingCollection<string>(new ConcurrentQueue<string>());

        static AsyncLogModule()
        {
            var thread = new Thread(ThreadMain);
            thread.IsBackground = true;
            thread.Start();
        }

        public void Init(HttpApplication context)
        {
            context.LogRequest += LogRequestEvent;
        }

        public void LogRequestEvent(object sender, EventArgs e)
        {
            var context = (HttpApplication)sender;

            s_queue.Add(context.Request.Url.ToString());
        }

        private static void ThreadMain()
        {
            foreach (var data in s_queue.GetConsumingEnumerable())
            {
                Trace.WriteLine(data);
                //Thread.Sleep(500);
            }
        }

        public void Dispose()
        {
        }
    }
}
編集 履歴 (0)

このやり方はちょっと筋が悪いです。
リクエストの度にスレッドプールの待ちキューにタスクが入ってしまうため、タスクがあふれてしまい、かなり後にならないとログが出力されないなどが起こります。
また、リクエストの処理とスレッドの奪い合いに近い状態になり、本来のリクエストが滞ることもあります。
DB接続などのリソース確保が頻繁になり、効率も低下しやすくなります。

方向としては、まず、ログ出力を要求するタイミングではキューに出力データを同期的に追加するだけにしておき、実際の出力処理は非同期でキューにデータが入ったときに起動するようにします。このとき、データごとに非同期処理を起動するのではなく、キュー内の全データをまとめて出力し、データが無くなれば非同期処理を終えるような流れにします。
このようにすることで、非同期のタスクが大量に発生する事を避けられ、スレッドの枯渇や非効率な処理を回避できます。
DBへの接続などのリソース準備も一括に出来ますので、そういう面でも効率的になります。
※まあ、これはアクセスログ程度なら問題ないこともありますが。(DBなら普通は接続プールを使うので問題ない可能性が高い)

編集 履歴 (1)
  • キューは、.NET 4以降ならBlockingCollection<T>とConcurrentQueue<T>の組み合わせが使えますかね。
    キューを読み出す側との連携がやや難易度高いかもしれません。
    スレッドプールよりは、専用のスレッドたてた方がいいです(作成するスレッドは一つで、データがない間は待機、BlockingCollection<T> だとたぶんこれが簡単に出来るはず)。
    -
  • 御回答頂き、ありがとうございます。
    頂いた内容を元に頑張っておりますが、まだ解決しておりません。
    -

解決いたしました。

今回、別スレッドが動かなかったのは、他のモジュールが邪魔をしていました。
追加モジュールは入っていないと思っていましたが、親ディレクトリ内に置かれた
web.config内で追加モジュールを指定されており、それに気付かず、そのモジュール
が原因で正常動作しておりませんでした。 御迷惑をお掛け致しました。

nachaさんからのアドバイスにより、下記の事を学びました。
nachaさん、御協力ありがとうございます。

・HttpApplication内でWebリクエストに関するレスポンス処理以外を行おうとしない事
・今回のようにリクエストとレスポンスに直接関係しない処理は、キューを使い
 HttpApplicationイベントではキューへの投入のみにする(軽い処理のみにする)
・HttpModuleのインスタンスも何個も作られる

反省点
・コンストラクタと言う手を忘れていた事
・継承される設定を忘れ、はまっていた事

                       すみません・・・ f(ーー;

以上、本件クローズとさせて頂きます。
皆さん、御協力ありがとうございました。 m(__)m

編集 履歴 (0)

まだ、解決に至っておりません。

nachaさんからのアドバイスなどを元に下記のような書き方に変更しましたが、
別スレッドが思うように動きません。何が悪いのでしょうか?

皆さん、解決方法や問題点などを教えて頂けないでしょうか。

以上、宜しくお願い致します。
 
 
下記ソースは、
HTTPモジュールを使い、Webアプリ全体の初期処理を行うInitで非同期な
ログを残すためのスレッドを起動させる。Webページアクセス時に発生する
LogRequestEventでログをLogQueueにセットしてWebページ処理を終らす。
Initで起動させた別スレッドでLogQueueを取得し、DBなどにログを残す
処理を行う。
と言うイメージで作りました。
 

// ASP.NET(C#) .Net Framework4.5
// IHttpModuleの継承クラス内

private statc Thread LogThread = null;
private statc Queue<LogCollections> LogQueue;

public void Init(HttpApplication context)
{
    LogQueue = new Queue<LogCollections>();

    LogThread = new Thread(new ThreadStart(SetLog));
    LogThread.IsBackground = true;
    LogThread.Start();

    context.LogRequest += new EventHandler(LogRequestEvent);
}

public void LogRequestEvent(object sender, EventArgs e)
{
    HttpApplication app = (HttpApplication)sender;
    LogCollections lc = new LogCollections(app);
    LogQueue.Enqueue(lc);
}

private void SetLog()
{
    while (1) {
       Thread.Sleep(5000);
       for (int i = 0 ; i < LogQueue.Count ; i++) {
           LogCollections lc = LogQueue.Dequeue();
           // ログを残す処理
       }
    }
}
編集 履歴 (1)
  • すみません、コメント位置を間違えました。
    ええと、とりあえずは私が書いたやり方で正確に作ってみてください。
    作成するコレクションの種類、作成タイミングはInitではなくstaticコンストラクタかフィールドの宣言と同時で、スレッド作成もInitではなくstaticコンストラクタ、ループはきちんとGetCosumingEnumerableをforeachてループ、こんなところですかね。
    -
  • ちなみに、HttpApplicationは、ログ要求イベント内だけで使うようにする必要があります。非同期の処理側では触ってはいけません。 -
  • 追加の回答にサンプルを書いてみたので参考にしてみてください。 -
  • ついでにちなみに、Initはアプリケーション全体て一回実行ではないので注意してください。何回も実行されます。HttpModuleのインスタンスも何個も作られます。各インスタンスにつきInit一回実行です。 -

まだ解決には至っておりません。 やはり別スレッドを準備できずにいます。
何をすれば解決するのでしょうか。

皆さん、解決方法や問題点などを教えて頂けないでしょうか。

以上、宜しくお願い致します。

現在、nachaさんからの有力な情報を元にInitではなく、下記コードのように
staticで別スレッドを作成する処理を組込みましたが、TEST1は出力されるが
TEST2は出力されませんでした。

 public class TESTModule : IHttpModule
{
    static TESTModule()
    {
        Thread TESTThread = new Thread(new ThreadStart(TESTThread));
        TESTThread.IsBackground = true;
        TESTThread.Start();
        // TEST1をファイルに出力する処理を試しに入れてみた
    }

    private static void TESTThread()
    {
        // TEST2をファイルに出力する処理を試しに入れてみた
    }

nachaさんへ
 コードまで頂き、ありがとうございますです。
 まだ解決しておりません。何がいけないのでしょうか・・・
 動かないのは私の環境下だけなのでしょうか?

編集 履歴 (0)
  • んーどうもよくわかりませんね…私のサンプルは基本そのままで期待通り動いているものです。トレース出力などは適宜変えてもらうとして、概ねそのままで動かないですか?
    スレッドが起動してないというのもおかしいですね。ファイル出力で変なとこに出してスレッドが動き出す前に即座に再起動されてしまってるとかのオチは無いですよね…?
    -

一応、簡単に実装のイメージというか流れを書いておきます。

まず、IHttpModuleのstaticなメンバとして、ConcurrentQueueをバッキングストアとして使用するBlockingCollectionを作成します。今回の例ならTはintですね。

また、statcなコンストラクタでスレッドを一つ起動します。
スレッドのメソッド内容は後述します。

ハンドリングされるLogRequestEventでは、staticメンバのコレクションにデータをAddだけします。

スレッドのメソッドでは、foreachでコレクションのGetConsumingEnumerable()の結果をループします。
ループ内で、DB接続を開いて、取り出したデータを出力し、接続を閉じます。
DB接続はかならず接続プールを有効にしておいてください。

おおよそこんな感じです。
凝ると色々改善点はありますが。

編集 履歴 (1)
ウォッチ

この質問への回答やコメントをメールでお知らせします。