异步记录器。我可以丢失/延迟日志条目吗?

时间:2012-08-25 04:05:44

标签: c#-4.0 thread-safety task-parallel-library

我正在实现自己的日志框架。以下是我的BaseLogger,它接收日志条目并将其推送到实现abstract Log方法的实际Logger。

我使用C#TPL以异步方式登录。我使用Threads而不是TPL。 (TPL任务没有真正的线程。所以如果应用程序的所有线程都结束,任务也将停止,这将导致所有“等待”日志条目丢失。)

public abstract class BaseLogger
{
    // ... Omitted properties constructor .etc. ... //

public virtual void AddLogEntry(LogEntry entry)
{
    if (!AsyncSupported)
    {
        // the underlying logger doesn't support Async.
        // Simply call the log method and return.
        Log(entry);
        return;
    }
    // Logger supports Async.
    LogAsync(entry);
}

private void LogAsync(LogEntry entry)
{
    lock (LogQueueSyncRoot) // Make sure we ave a lock before accessing the queue.
    {
        LogQueue.Enqueue(entry);
    }

    if (LogThread == null || LogThread.ThreadState == ThreadState.Stopped)
    { // either the thread is completed, or this is the first time we're logging to this logger.
        LogTask = new  new Thread(new ThreadStart(() =>
        {
            while (true)
            {
                LogEntry logEntry;
                lock (LogQueueSyncRoot)
                {
                    if (LogQueue.Count > 0)
                    {
                        logEntry = LogQueue.Dequeue();
                    }
                    else
                    {
                        break; 
                        // is it possible for a message to be added,
                        // right after the break and I leanve the lock {} but 
                        // before I exit the loop and task gets 'completed' ??
                    }
                }
                Log(logEntry);
             }
        }));
        LogThread.Start();
    }
}

// Actual logger implimentations will impliment this method.
protected abstract void Log(LogEntry entry);
}

请注意,AddLogEntry可以同时从多个线程调用。

我的问题是,此实现是否可能丢失日志条目? 我担心,是否有可能在队列中添加一个日志条目,就在我的线程存在带有break语句的循环并退出锁定块,并且在else子句中,并且线程仍在'跑'状态。

我确实意识到,因为我正在使用队列,即使我错过了一个条目,下一个记录请求也将推送错过的条目。但这是不可接受的,特别是如果应用程序的最后一个日志条目发生这种情况。

另外,请告诉我是否以及如何实现相同的功能,但请使用新的C#5.0 asyncawait关键字以及更清晰的代码。我不介意要求.NET 4.5。

先谢谢。

3 个答案:

答案 0 :(得分:4)

虽然你可能会让这个工作,但根据我的经验,我建议,如果可能的话,使用现有的日志记录框架:)例如,log4net的异步日志记录/ appender有各种选项,例如{{ 3}}

否则,恕我直言,因为你要在日志记录操作期间阻塞线程池线程,我只需要为你的日志记录启动一个专用线程。你似乎已经采用了这种方法,只需通过Task,这样你就不会在没有任何记录的情况下持有线程池线程。但是,实现的简化我认为只是拥有专用线程。

拥有专用日志记录线程后,您只需要一个中间this async appender wrapper thingy。此时,您的日志方法只会添加到队列中,您的专用日志记录线程就会执行您已经拥有的循环。如果您需要阻止/限制行为,可以使用ConcurrentQueue进行换行。

通过将专用线程作为唯一写入的东西,它消除了让多个线程/任务拉出队列条目并尝试同时写入日志条目(痛苦的竞争条件)的任何可能性。由于log方法现在只是添加到集合中,因此它不需要是异步的,您根本不需要处理TPL,这使得它更简单,更容易推理(并且希望在'显然是正确的'或左右:)

这个'专用日志记录线程'方法是我认为我链接的log4net appender也是如此,FWIW,以帮助作为一个例子。

答案 1 :(得分:3)

我发现两个竞争条件脱离了我的头脑:

  1. 如果多个线程调用Thread,您可以启动多个AddLogEntry。这不会导致丢失事件,但效率低下。
  2. 是的,当Thread退出时,事件可以排队,在这种情况下,它会“丢失”。
  3. 此外,这里存在一个严重的性能问题:除非您不断记录(每秒数千次),否则您将为每个日志条目启动一个新的Thread。这将很快变得昂贵。

    与James一样,我同意您应该使用已建立的日志记录库。记录并不像看起来那么简单,而且已经有很多解决方案。

    也就是说,如果你想要一个不错的基于.NET 4.5的方法,那很简单:

    public abstract class BaseLogger
    {
      private readonly ActionBlock<LogEntry> block;
    
      protected BaseLogger(int maxDegreeOfParallelism = 1)
      {
        block = new ActionBlock<LogEntry>(
            entry =>
            {
              Log(entry);
            },
            new ExecutionDataflowBlockOptions
            {
              MaxDegreeOfParallelism = maxDegreeOfParallelism,
            });
      }
    
      public virtual void AddLogEntry(LogEntry entry)
      {
        block.Post(entry);
      }
    
      protected abstract void Log(LogEntry entry);
    }
    

答案 2 :(得分:0)

关于由于未处理的异常导致app crush的等待消息丢失,我已经为事件AppDomain.CurrentDomain.DomainUnload绑定了一个处理程序。是这样的:

protected ManualResetEvent flushing = new ManualResetEvent(true);
protected AsyncLogger()  // ctor of logger
{
    AppDomain.CurrentDomain.DomainUnload += CurrentDomain_DomainUnload;
}

protected void CurrentDomain_DomainUnload(object sender, EventArgs e)
{
    if (!IsEmpty)
    {
        flushing.WaitOne();
    }
}

也许不是太干净,但有效。