如何有效地异步登录?

时间:2009-07-25 08:11:44

标签: c# multithreading logging enterprise-library

我在我的一个项目中使用Enterprise Library 4进行日志记录(以及其他用途)。我注意到我正在做的日志记录有一些成本,我可以通过在单独的线程上进行日志记录来缓解这些成本。

我现在这样做的方法是创建一个LogEntry对象,然后在调用Logger.Write的委托上调用BeginInvoke。

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

我真正想做的是将日志消息添加到队列中,然后让一个线程将LogEntry实例从队列中拉出并执行日志操作。这样做的好处是日志记录不会干扰执行操作,并且并非每个日志记录操作都会导致在线程池上抛出作业。

如何以线程安全的方式创建支持多个编写器和一个读取器的共享队列?设计用于支持许多编写器(不会导致同步/阻塞)和单个读取器的队列实现的一些示例将非常受欢迎。

关于替代方法的建议也值得赞赏,但我对改变日志框架并不感兴趣。

10 个答案:

答案 0 :(得分:34)

我前一段时间写了这段代码,随意使用它。

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();
    }
}

优于原版:

  • 这是一次性的,所以你可以摆脱异步记录器
  • 刷新语义得到改进
  • 它会稍微好一点,然后是沉默

答案 1 :(得分:11)

是的,您需要一个生产者/消费者队列。我在线程教程中有一个例子 - 如果你查看我的"deadlocks / monitor methods"页面,你会在下半部分找到代码。

当然,网上还有很多其他的例子 - 而.NET 4.0也将在框架中附带一个(比我的更多功能!)。在.NET 4.0中,您可能会在ConcurrentQueue<T>中包含BlockingCollection<T>

该页面上的版本是非泛型的(之前写的是 long ),但你可能想让它变得通用 - 这样做很简单。

你可以从每个“普通”线程调用Produce,从一个线程调用Consume,只需循环并记录它所消耗的任何内容。将消费者线程作为后台线程可能是最容易的,因此您不必担心在应用程序退出时“停止”队列。这确实意味着有一个远程可能性错过了最终的日志条目(如果它是在应用程序退出时写入它的一半) - 或者如果你的生产速度超过消耗/日志的速度,那就更多了。

答案 2 :(得分:2)

我建议开始测量整个系统上记录的实际性能影响(即通过运行分析器),并可选择切换到更快的速度,如log4net(我个人已迁移到它来自EntLib很久以前的日志记录)。

如果这不起作用,您可以尝试使用.NET Framework中的这个简单方法:

ThreadPool.QueueUserWorkItem
  

对执行方法进行排队。该方法在线程池线程可用时执行。

MSDN Details

如果这不起作用,那么你可以求助于John Skeet提供的东西,并亲自编写异步记录框架。

答案 3 :(得分:2)

这是我想出来的......也看到Sam Saffron的回答。这个答案是社区维基,以防人们在代码中看到任何问题并想要更新。

/// <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;
                }
            }
        }
    }
}

答案 4 :(得分:1)

为了回应Sam Safrons的帖子,我想调用flush并确保一切都写完了。在我的情况下,我正在写入队列线程中的数据库,并且我的所有日​​志事件都排队等待,但有时候应用程序在所有内容完成之前停止,这在我的情况下是不可接受的。我更改了几个代码块,但我想分享的主要内容是flush:

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();
                }
            }
        }

我希望拯救某人有些沮丧。在并行进程中记录大量数据尤其明显。

感谢您分享您的解决方案,这让我有了一个好的方向!

- Johnny S

答案 5 :(得分:1)

我想说我以前的帖子有点无用。您可以简单地将AutoFlush设置为true,而不必遍历所有侦听器。但是,我仍然遇到了试图刷新记录器的并行线程的疯狂问题。我必须创建另一个在复制队列期间设置为true的布尔值并执行LogEntry写入然后在刷新例程中我必须检查该布尔值以确保某些东西不在队列中且没有任何东西被处理在回来之前。

现在并行多个线程可以触及这个东西,当我调用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);
        }
    }

答案 6 :(得分:1)

只是更新:

使用带有.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/

答案 7 :(得分:0)

额外的间接水平可能对此有所帮助。

您的第一个异步方法调用可以将消息放入同步的队列并设置一个事件 - 因此锁定发生在线程池中,而不是发生在您的工作线程上 - 然后还有另一个线程将消息从队列中拉出当事件被提出时。

答案 8 :(得分:0)

如果您在单独的线程上记录某些内容,则在应用程序崩溃时可能无法写入该消息,这使得它无用。

原因在于为什么你应该在每次书面录入后总是冲洗。

答案 9 :(得分:0)

如果你想到的是一个SHARED队列,那么我认为你将不得不同步它的写入,推送和弹出。

但是,我仍然认为值得针对共享队列设计。与日志记录的IO相比,可能与您的应用程序正在进行的其他工作相比,推送和弹出窗口的短暂阻塞量可能不会很大。