如何有效地异步登录?

Eri*_*ver 32 c# logging multithreading enterprise-library

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

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

new Action<LogEntry>(Logger.Write).BeginInvoke(le, null, null);
Run Code Online (Sandbox Code Playgroud)

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

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

关于替代方法的建议也将受到赞赏,但我对改变日志框架并不感兴趣.

Sam*_*ron 35

我不久前写了这段代码,随意使用它.

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

一些优点:

  • 它使后台记录器保持活动状态,因此不需要旋转和旋转线程.
  • 它使用单个线程来为队列提供服务,这意味着永远不会有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();
    }
}
Run Code Online (Sandbox Code Playgroud)

优于原版:

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

  • 如果从多个线程使用它,那么"等待"标志应该是易失性的 - 或者在访问它时使用锁定.我不确定我是否在等待10秒而不是仅仅等待没有超时.我还叫套装,而你仍然持有锁 - 它实际上并不会带来多大的改变,但你可以让两个线程都添加事件,然后一个线程设置事件,读者完成等待和复制数据,*然后*另一位作家再次设置该事件.不可否认,没有太大的损失.为什么你有loggingThread实例变量? (2认同)
  • 10 秒的等待是为了避免饥饿,以防您收到部分服务的突发事件。(在复制队列后立即获取事件)我可能应该将其更改为手动重置事件,并且可能添加一些逻辑来重新服务队列,直到它真正为空,然后再等待。线程 var 只是一点点未来证明,并不是真正需要的(以防我想获得线程的 id 或其他东西)。 (2认同)
  • 是的,但是您必须管理单个后台工作人员的生命周期,它实际上不是工作的工具,因为您不需要随附的进度工具等. (2认同)

Jon*_*eet 11

是的,您需要一个生产者/消费者队列.我在我的线程教程中有一个这样的例子 - 如果你查看我的"死锁/监视方法"页面,你会在下半部分找到代码.

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

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

你可以Produce从每个"普通"线程调用,Consume从一个线程调用,只是循环并记录它消耗的任何东西.将消费者线程作为后台线程可能是最简单的,因此您不必担心在应用程序退出时"停止"队列.这确实意味着很可能错过了最终的日志条目(如果它是在应用程序退出时写入它的一半) - 或者甚至更多,如果你的生产速度超过消耗/日志的速度.

  • 我更喜欢锁定单独的对象,我绝对*知道*其他任何东西都无法锁定.我不知道Queue是否在内部锁定自己...在这种情况下,如果它确实无关紧要,但作为一般原则,我喜欢保持锁"私有"(所以没有别的可以干涉),除非我有这是另一个很好的理由. (3认同)