web-dev-qa-db-ja.com

非同期で効果的にログを記録する方法は?

ロギング(およびその他の目的)のために、プロジェクトの1つでEnterprise Library 4を使用しています。別のスレッドでログを記録することで軽減できる、実行中のログ記録にはコストがかかることに気づきました。

これを行う方法は、LogEntryオブジェクトを作成してから、Logger.Writeを呼び出すデリゲートでBeginInvokeを呼び出すことです。

new Action<LogEntry>(Logger.Write).BeginInvoke(le, null, null);

私が本当にやりたいことは、ログメッセージをキューに追加し、シングルスレッドでLogEntryインスタンスをキューからプルして、ログ操作を実行することです。これの利点は、ロギングによって実行中の操作が妨げられることはなく、すべてのロギング操作がジョブをスレッドプールにスローするわけではないことです。

スレッドセーフな方法で多くのライターと1つのリーダーをサポートする共有キューを作成するにはどうすればよいですか?多くのライター(同期/ブロックを引き起こさない)と単一のリーダーをサポートするように設計されたキュー実装のいくつかの例は、本当にありがたいです。

代替アプローチに関する推奨事項も高く評価されますが、私はロギングフレームワークの変更には興味がありません。

30
Eric Schoonover

私はしばらく前にこのコードを書いたので、自由に使ってください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace MediaBrowser.Library.Logging {
    public abstract class ThreadedLogger : LoggerBase {

        Queue<Action> queue = new Queue<Action>();
        AutoResetEvent hasNewItems = new AutoResetEvent(false);
        volatile bool waiting = false;

        public ThreadedLogger() : base() {
            Thread loggingThread = new Thread(new ThreadStart(ProcessQueue));
            loggingThread.IsBackground = true;
            loggingThread.Start();
        }


        void ProcessQueue() {
            while (true) {
                waiting = true;
                hasNewItems.WaitOne(10000,true);
                waiting = false;

                Queue<Action> queueCopy;
                lock (queue) {
                    queueCopy = new Queue<Action>(queue);
                    queue.Clear();
                }

                foreach (var log in queueCopy) {
                    log();
                }
            }
        }

        public override void LogMessage(LogRow row) {
            lock (queue) {
                queue.Enqueue(() => AsyncLogMessage(row));
            }
            hasNewItems.Set();
        }

        protected abstract void AsyncLogMessage(LogRow row);


        public override void Flush() {
            while (!waiting) {
                Thread.Sleep(1);
            }
        }
    }
}

いくつかの利点:

  • バックグラウンドロガーを存続させるため、スレッドをスピンアップおよびスピンダウンする必要はありません。
  • これは、単一のスレッドを使用してキューにサービスを提供します。つまり、100のスレッドがキューにサービスを提供している状況は決してありません。
  • キューをコピーして、ログ操作の実行中にキューがブロックされないようにします
  • AutoResetEventを使用して、bgスレッドが待機状態であることを確認します
  • それは、私見、非常に簡単です

これは少し改良されたバージョンです。私はそれに対してほとんどテストを実行していませんが、いくつかの小さな問題に対処しています。

public abstract class ThreadedLogger : IDisposable {

    Queue<Action> queue = new Queue<Action>();
    ManualResetEvent hasNewItems = new ManualResetEvent(false);
    ManualResetEvent terminate = new ManualResetEvent(false);
    ManualResetEvent waiting = new ManualResetEvent(false);

    Thread loggingThread; 

    public ThreadedLogger() {
        loggingThread = new Thread(new ThreadStart(ProcessQueue));
        loggingThread.IsBackground = true;
        // this is performed from a bg thread, to ensure the queue is serviced from a single thread
        loggingThread.Start();
    }


    void ProcessQueue() {
        while (true) {
            waiting.Set();
            int i = ManualResetEvent.WaitAny(new WaitHandle[] { hasNewItems, terminate });
            // terminate was signaled 
            if (i == 1) return; 
            hasNewItems.Reset();
            waiting.Reset();

            Queue<Action> queueCopy;
            lock (queue) {
                queueCopy = new Queue<Action>(queue);
                queue.Clear();
            }

            foreach (var log in queueCopy) {
                log();
            }    
        }
    }

    public void LogMessage(LogRow row) {
        lock (queue) {
            queue.Enqueue(() => AsyncLogMessage(row));
        }
        hasNewItems.Set();
    }

    protected abstract void AsyncLogMessage(LogRow row);


    public void Flush() {
        waiting.WaitOne();
    }


    public void Dispose() {
        terminate.Set();
        loggingThread.Join();
    }
}

元の上の利点:

  • 使い捨てなので、非同期ロガーを取り除くことができます
  • フラッシュのセマンティクスが改善されました
  • それは、静寂が続くバーストに少し良く反応します
34
Sam Saffron

はい、プロデューサー/コンシューマーキューが必要です。私のスレッディングチュートリアルにこの例が1つあります。私の "deadlocks/monitor methods" ページを見ると、後半にコードがあります。

もちろん、オンラインには他にもたくさんの例があります。NET4.0は、フレームワークにも1つ付属しています(私のものよりも完全に機能しています!)。 .NET 4.0では、おそらく ConcurrentQueue<T>BlockingCollection<T>

そのページのバージョンは一般的ではありません(long以前に書かれています)おそらくあなたはそれを一般的にしたいと思うでしょう-それは取るに足らない。

各「通常の」スレッドからProduceを呼び出し、1つのスレッドからConsumeを呼び出して、ループし、消費したものをログに記録します。コンシューマースレッドをバックグラウンドスレッドにするのがおそらく最も簡単なので、アプリが終了するときにキューを「停止」することを心配する必要はありません。つまり、最終的なログエントリが失われる可能性はかなりあります(アプリの終了時に書き込みの途中である場合)。さらに、消費/ログできるよりも速く生成している場合はさらに多くなります。

11
Jon Skeet

これが私が思いついたものです... Sam Saffronの回答も参照してください。この回答は、コードに表示されて更新したい問題がある場合のコミュニティWikiです。

/// <summary>
/// A singleton queue that manages writing log entries to the different logging sources (Enterprise Library Logging) off the executing thread.
/// This queue ensures that log entries are written in the order that they were executed and that logging is only utilizing one thread (backgroundworker) at any given time.
/// </summary>
public class AsyncLoggerQueue
{
    //create singleton instance of logger queue
    public static AsyncLoggerQueue Current = new AsyncLoggerQueue();

    private static readonly object logEntryQueueLock = new object();

    private Queue<LogEntry> _LogEntryQueue = new Queue<LogEntry>();
    private BackgroundWorker _Logger = new BackgroundWorker();

    private AsyncLoggerQueue()
    {
        //configure background worker
        _Logger.WorkerSupportsCancellation = false;
        _Logger.DoWork += new DoWorkEventHandler(_Logger_DoWork);
    }

    public void Enqueue(LogEntry le)
    {
        //lock during write
        lock (logEntryQueueLock)
        {
            _LogEntryQueue.Enqueue(le);

            //while locked check to see if the BW is running, if not start it
            if (!_Logger.IsBusy)
                _Logger.RunWorkerAsync();
        }
    }

    private void _Logger_DoWork(object sender, DoWorkEventArgs e)
    {
        while (true)
        {
            LogEntry le = null;

            bool skipEmptyCheck = false;
            lock (logEntryQueueLock)
            {
                if (_LogEntryQueue.Count <= 0) //if queue is empty than BW is done
                    return;
                else if (_LogEntryQueue.Count > 1) //if greater than 1 we can skip checking to see if anything has been enqueued during the logging operation
                    skipEmptyCheck = true;

                //dequeue the LogEntry that will be written to the log
                le = _LogEntryQueue.Dequeue();
            }

            //pass LogEntry to Enterprise Library
            Logger.Write(le);

            if (skipEmptyCheck) //if LogEntryQueue.Count was > 1 before we wrote the last LogEntry we know to continue without double checking
            {
                lock (logEntryQueueLock)
                {
                    if (_LogEntryQueue.Count <= 0) //if queue is still empty than BW is done
                        return;
                }
            }
        }
    }
}
2
Eric Schoonover

システム全体のロギングの実際のパフォーマンスへの影響を測定し、オプションで log4net (私はかなり前に個人的にEntLibロギングからそれに移行しました)。

これが機能しない場合は、.NET Frameworkの次の簡単な方法を使用してみてください。

ThreadPool.QueueUserWorkItem

実行するメソッドをキューに入れます。このメソッドは、スレッドプールスレッドが使用可能になったときに実行されます。

MSDNの詳細

これも機能しない場合は、John Skeetが提供するような手段を使用して、実際に非同期ロギングフレームワークを自分でコーディングできます。

2
Rinat Abdullin

Sam Safronsの投稿に応えて、私はフラッシュを呼び出して、すべての書き込みが本当に終了したことを確認したかった。私の場合、キュースレッドでデータベースに書き込みを行っており、すべてのログイベントがキューに入れられていましたが、すべての書き込みが完了する前にアプリケーションが停止することがあり、私の状況では受け入れられません。私はあなたのコードのいくつかのチャンクを変更しましたが、私が共有したかった主なものはフラッシュでした:

public static void FlushLogs()
        {   
            bool queueHasValues = true;
            while (queueHasValues)
            {
                //wait for the current iteration to complete
                m_waitingThreadEvent.WaitOne();

                lock (m_loggerQueueSync)
                {
                    queueHasValues = m_loggerQueue.Count > 0;
                }
            }

            //force MEL to flush all its listeners
            foreach (MEL.LogSource logSource in MEL.Logger.Writer.TraceSources.Values)
            {                
                foreach (TraceListener listener in logSource.Listeners)
                {
                    listener.Flush();
                }
            }
        }

私はそれが誰かにいくつかの欲求不満を救うと思います。これは、大量のデータをログに記録する並列プロセスで特に顕著です。

あなたの解決策を共有してくれてありがとう、それは私を良い方向に導きました!

-ジョニーS

1
Johnny Serrano

以前の投稿は役に立たなかったと言いたいです。 AutoFlushをtrueに設定するだけで、すべてのリスナーをループする必要がなくなります。しかし、ロガーをフラッシュしようとする並列スレッドで、まだクレイジーな問題がありました。キューのコピーとLogEntry書き込みの実行中にtrueに設定される別のブール値を作成する必要があり、フラッシュルーチンでそのブール値をチェックして、何かがまだキューになく、何も処理されていないことを確認する必要がありました。戻る前に。

これで、複数のスレッドが並行してこの問題に遭遇する可能性があり、flushを呼び出すと、本当にフラッシュされていることがわかります。

     public static void FlushLogs()
    {
        int queueCount;
        bool isProcessingLogs;
        while (true)
        {
            //wait for the current iteration to complete
            m_waitingThreadEvent.WaitOne();

            //check to see if we are currently processing logs
            lock (m_isProcessingLogsSync)
            {
                isProcessingLogs = m_isProcessingLogs;
            }

            //check to see if more events were added while the logger was processing the last batch
            lock (m_loggerQueueSync)
            {
                queueCount = m_loggerQueue.Count;
            }                

            if (queueCount == 0 && !isProcessingLogs)
                break;

            //since something is in the queue, reset the signal so we will not keep looping

            Thread.Sleep(400);
        }
    }
1
Johnny Serrano

ただの更新:

.NET 4.0でentepriseライブラリ5.0を使用すると、次の方法で簡単に実行できます。

static public void LogMessageAsync(LogEntry logEntry)
{
    Task.Factory.StartNew(() => LogMessage(logEntry)); 
}

参照: http://randypaulo.wordpress.com/2011/07/28/c-enterprise-library-asynchronous-logging/

1
John

追加レベルの間接参照がここで役立ちます。

最初の非同期メソッド呼び出しは、同期されたキューにメッセージを書き込み、イベントを設定できます。これにより、ロックはワーカースレッドではなくスレッドプールで発生します。その後、イベントが発生すると、別のスレッドがキューからメッセージをプルします。発生します。

0
Steve Gilham

別のスレッドで何かをログに記録すると、アプリケーションがクラッシュした場合にメッセージが書き込まれず、役に立たなくなります。

理由は、書き込みを行うたびに常にフラッシュする必要がある理由です。

0
leppie

念頭に置いているのがSHAREDキューの場合、書き込み、プッシュ、ポップを同期する必要があると思います。

しかし、それでも、共有キューの設計を目指す価値はあると思います。ロギングのIO=と比較して、おそらくアプリが実行している他の作業と比較して、プッシュとポップのブロックの短い量はおそらく重要ではありません。

0
Corey Trager