【问题标题】:Async Logger. Can I lose/delay log entries?异步记录器。我可以丢失/延迟日志条目吗?
【发布时间】:2017-10-28 01:11:45
【问题描述】:

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

我使用 C# TPL 以异步方式登录。 我使用线程而不是 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。

提前致谢。

【问题讨论】:

  • 啊……搞砸了。这里是午夜。我正在抓取这个并使用 NLog XD ...谢谢大家的帮助。

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


【解决方案1】:

虽然您可能会使其工作,但根据我的经验,如果可能的话,我建议您使用现有的日志框架 :) 例如,使用 log4net 的异步日志/附加程序有多种选项,例如 @987654321 @。

否则,恕我直言,因为无论如何您都会在日志记录操作期间阻塞线程池线程,所以我会改为为您的日志记录启动一个专用线程。您似乎已经开始采用这种方法,只是通过 Task ,这样当没有记录时您就不会持有线程池线程。但是,我认为简化实现对拥有专用线程很有好处。

一旦你有了一个专用的日志线程,你就只需要一个中间的ConcurrentQueue。那时,您的日志方法只是添加到队列中,而您的专用日志线程只是执行您已经拥有的 while 循环。如果您需要阻塞/有界行为,可以使用 BlockingCollection 包装。

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

这种“专用日志记录线程”方法是我认为我链接到的 log4net appender 也可以做到的,FWIW,以防有助于作为示例。

【讨论】:

  • 所以....我的实现是否有可能错过日志条目?以上述方式,在break 和任务完成之间?
  • 请注意..我更新了代码。我按照您的建议使用线程,因为它会变得讨厌并且在应用程序结束时会丢失“等待”条目。
【解决方案2】:

我突然想到了两个比赛条件:

  1. 如果多个线程调用AddLogEntry,您可以启动多个Thread。这不会导致丢失事件,但效率低。
  2. 是的,一个事件可以在Thread 退出时排队,在这种情况下它会“丢失”。

此外,这里还有一个严重的性能问题:除非您不断地记录(每秒数千次),否则您将为每个日志条目旋转一个新的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);
}

【讨论】:

    【解决方案3】:

    关于由于未处理的异常而导致应用程序崩溃的等待消息丢失,我已将处理程序绑定到事件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();
        }
    }
    

    也许不太干净,但可以。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-24
      • 1970-01-01
      • 1970-01-01
      • 2014-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多