我正在实现自己的日志框架。以下是我的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 async
和await
关键字以及更清晰的代码。我不介意要求.NET 4.5。
先谢谢。
答案 0 :(得分:4)
虽然你可能会让这个工作,但根据我的经验,我建议,如果可能的话,使用现有的日志记录框架:)例如,log4net的异步日志记录/ appender有各种选项,例如{{ 3}}
否则,恕我直言,因为你要在日志记录操作期间阻塞线程池线程,我只需要为你的日志记录启动一个专用线程。你似乎已经采用了这种方法,只需通过Task,这样你就不会在没有任何记录的情况下持有线程池线程。但是,实现的简化我认为只是拥有专用线程。
拥有专用日志记录线程后,您只需要一个中间this async appender wrapper thingy。此时,您的日志方法只会添加到队列中,您的专用日志记录线程就会执行您已经拥有的循环。如果您需要阻止/限制行为,可以使用ConcurrentQueue进行换行。
通过将专用线程作为唯一写入的东西,它消除了让多个线程/任务拉出队列条目并尝试同时写入日志条目(痛苦的竞争条件)的任何可能性。由于log方法现在只是添加到集合中,因此它不需要是异步的,您根本不需要处理TPL,这使得它更简单,更容易推理(并且希望在'显然是正确的'或左右:)
这个'专用日志记录线程'方法是我认为我链接的log4net appender也是如此,FWIW,以帮助作为一个例子。
答案 1 :(得分:3)
我发现两个竞争条件脱离了我的头脑:
Thread
,您可以启动多个AddLogEntry
。这不会导致丢失事件,但效率低下。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);
}
答案 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();
}
}
也许不是太干净,但有效。