【问题标题】:How to effectively log asynchronously?如何有效地异步记录?
【发布时间】:2010-11-13 23:19:29
【问题描述】:

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

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

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

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

如何以线程安全的方式创建一个支持多个写入器和一个读取器的共享队列?非常感谢一些旨在支持多个写入器(不会导致同步/阻塞)和单个读取器的队列实现示例。

也将不胜感激有关替代方法的建议,但我对更改日志记录框架不感兴趣。

【问题讨论】:

  • @spoon,我添加了一个稍微改进的版本,请记住,在使用它之前您必须对其进行大量测试,因为我刚刚敲了它。
  • 虽然不直接适用于这个问题(因为它说的是 EntLib 4),但 Enterprise Library 6 现在支持开箱即用的异步登录。

标签: c# multithreading logging enterprise-library


【解决方案1】:

这段代码是我不久前写的,请随意使用。

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

相对于原版的优势:

  • 它是一次性的,因此您可以摆脱异步记录器
  • 改进了刷新语义
  • 它会稍微更好地响应突发然后是沉默

【讨论】:

  • 如果你的“等待”标志被多个线程使用,它应该是易变的 - 或者在访问它时使用锁。我不确定我是否看到等待 10 秒的意义,而不是仅仅等待而没有超时。当你仍然持有锁时,我也会调用 Set - 它实际上并没有太大区别,但你可以让两个线程都添加事件,然后一个线程设置事件,读取器完成等待并复制数据,然后另一个作家再次设置事件。诚然,没有太大的损失。为什么会有 loggingThread 实例变量?
  • 10 秒的等待是为了避免饥饿,在您获得部分服务的突发事件的情况下。 (在复制队列后获取事件)我可能应该将其更改为手动重置事件,并可能添加一些逻辑来重新服务队列,直到它真正为空,然后再次等待。 thread var 只是一点点未来的证明,并不是真正需要的(以防我想获取线程的 id 或其他东西)。
  • @spoon,yerp 有一个优势,通过使用单个线程,您可以更优雅地序列化访问,在线程池场景中,您最终可能会有 100 个线程为队列提供服务,这是不可取的跨度>
  • 是的,但是您必须管理单个后台工作人员的生命周期,它确实不是工作的工具,因为您不需要它附带的进度设施等。
  • 在 hasNewItems.Reset() 之前;放置一个小延迟可能是个好主意,比如 10 毫秒,以允许队列建立一些数据。
【解决方案2】:

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

当然,网上还有很多其他示例 - .NET 4.0 也将在框架中附带一个(比我的功能更全!)。在 .NET 4.0 中,您可能会将 ConcurrentQueue&lt;T&gt; 包装在 BlockingCollection&lt;T&gt; 中。

该页面上的版本是非通用版本(它是在 很久 之前编写的),但您可能希望使其成为通用版本 - 这样做很简单。

你会从每个“正常”线程调用Produce,从一个线程调用Consume,只是循环并记录它消耗的任何内容。让消费者线程成为后台线程可能是最简单的,因此您无需担心应用程序退出时“停止”队列。这确实意味着很可能会丢失最终的日志条目(如果它在应用程序退出时写入它的一半) - 如果您的生产速度超过它可以消耗/记录的速度,甚至更多。

【讨论】:

  • 你能链接到描述这个的 .NET 4 文档吗?
  • 在 ProducerConsumer 示例中锁定对象实例与仅锁定 Queue 对象有什么区别?我现在只是在阅读您的文章的测试,所以我可能还没有达到解释。
  • 我更喜欢锁定单独的对象,我绝对知道没有其他东西会锁定。我不知道 Queue 是否在内部锁定自身......在这种情况下,如果它这样做并不重要,但作为一般原则,我喜欢保持锁定“私有”(所以没有其他东西可以干扰),除非我有一个很好的理由不这样做。
  • @Jon,我刚刚做了第二个实现,它支持 dispose 并使用手动重置事件。很想得到飞碟的批准印章:p
【解决方案3】:

我建议首先测量日志记录对整个系统的实际性能影响(即通过运行分析器),并可选择切换到更快的东西,如log4net(我个人已从EntLib 很久以前的日志记录)。

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

ThreadPool.QueueUserWorkItem

将一个方法排队等待执行。该方法在线程池线程可用时执行。

MSDN Details

如果这也不起作用,那么您可以求助于 John Skeet 提供的方法,并自己实际编写异步日志框架。

【讨论】:

  • 我已经确定异步日志记录有好处。此外,由于我们广泛使用 EntLib 日志记录,因此我无法切换日志记录框架。对于团队的其他成员来说,这不会是微不足道的或受欢迎的突破性变化:)。 ThreadPool.QueueUserWorkItem 基本上就是我现在使用的 BeginInvoke 调用。我认为约翰的回答是迄今为止最适合我的情况。
  • 对工作项进行排队并不能保证任何顺序,因此如果您关心记录消息的顺序,那么 IMO 将不是一种可行的方法。
【解决方案4】:

这是我想出的……另请参阅 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;
                }
            }
        }
    }
}

【讨论】:

  • 您似乎打算将 AsyncLogger 类设为单例。在这种情况下,我会将构造函数设为私有,但否则它似乎可以完成工作。
【解决方案5】:

为了回应 Sam Safrons 的帖子,我想打电话给 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();
                }
            }
        }

我希望这可以减轻一些人的挫败感。在记录大量数据的并行进程中尤其明显。

感谢您分享您的解决方案,它为我指明了一个好的方向!

--约翰尼S

【讨论】:

    【解决方案6】:

    我想说我之前的帖子有点没用。您可以简单地将 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);
            }
        }
    

    【讨论】:

    • 欢迎来到 Stack Overflow!我们在这里不像传统论坛那样工作。如果您有更新,您可以(并且应该)使用新信息/更正来编辑您的原始帖子,而不是添加新答案。
    【解决方案7】:

    只是更新:

    将企业库 5.0 与 .NET 4.0 一起使用,可以通过以下方式轻松完成:

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

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

    【讨论】:

    • 这会为每个日志消息添加一个线程,这可能非常昂贵且适得其反。正如问题中提到的@spoon16,排队以批量方式写入。
    • @M.Babcock 当你说这为每个日志消息添加一个线程时,你的意思是新线程?
    • @Mukund - 是的,如果发现日志记录需要超过 200-300 个 cpu 周期,那么它肯定会在新线程中执行。
    • 我对此表示赞同,但要告诉你,理想情况下,可能昂贵的 IO 操作不应该像这样使用Task.Run。它本质上是将同步调用转换为伪造的异步方法。如果库支持并使用 async/await 关键字(C# 5.0 后)调用它,则应该依赖真正的异步方法。请参阅 blog.stephencleary.com/2013/11/…blogs.msdn.com/b/pfxteam/archive/2012/03/24/10287244.aspx。只是说这没关系,它的工作,但不理想。
    • 这可能会给线程池带来很大压力。恕我直言,这不是一个好主意。
    【解决方案8】:

    额外的间接级别可能会有所帮助。

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

    【讨论】:

      【解决方案9】:

      如果你在一个单独的线程上记录一些东西,如果应用程序崩溃,消息可能不会被写入,这使得它相当无用。

      原因在于为什么您应该在每次写入后总是刷新。

      【讨论】:

      • 是的,我不将此方法用于异常日志记录或其他关键日志记录。我仅将它用于记录描述成功执行操作的信息状态消息。这些消息往往会更频繁地出现,我需要尽量减少它们的成本。
      【解决方案10】:

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

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

      【讨论】:

        猜你喜欢
        • 2023-03-09
        • 2013-06-05
        • 2017-08-06
        • 2016-01-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-02-15
        相关资源
        最近更新 更多