上篇博文提供了c++版异步日志类,本文提供同样功能的C#版的异步日志管理类。
C#环境下支持多线程的异步日志记录器的设计与实现
在现代应用程序开发中,日志记录是一项至关重要的任务,它帮助开发者追踪程序的运行情况,调试问题,并进行性能监控。特别是在高并发场景下,传统的同步日志记录方式可能会成为性能瓶颈。因此,设计一个高效、可扩展的异步日志记录器变得尤为重要。
本文将介绍一个简单的异步日志记录器类的实现,该记录器采用双缓冲区技术来提高效率,并利用 C# 的 ConcurrentQueue
和 SemaphoreSlim
等工具来保证线程安全和高性能。
设计理念
我们的目标是创建一个异步日志记录器,它可以:
- 收集来自多个线程的日志消息,并将它们异步地写入磁盘。
- 使用双缓冲区技术来避免在写盘过程中阻塞新的日志消息。
- 限制每次写盘的日志数量,以减少磁盘 I/O 操作次数。
- 根据时间间隔自动触发写盘操作。
实现细节
数据结构选择
我们选择了 ConcurrentQueue<T>
作为我们的缓冲区,因为它是一个线程安全的数据结构,可以在多线程环境下高效地添加和移除元素。此外,我们使用 SemaphoreSlim
来同步写盘操作,确保每次只有一个线程在写盘。
双缓冲区机制
为了提高效率,我们采用了双缓冲区机制。这意味着我们有两个缓冲区:一个活跃缓冲区用于接收新的日志消息,而另一个非活跃缓冲区用于写入磁盘。当非活跃缓冲区准备好写盘时,两个缓冲区的角色会互换。
关键代码
下面是我们异步日志记录器的核心代码:
public class AsyncLogger
{
private readonly string _logFilePath;
private readonly ConcurrentQueue<string> _activeBuffer;
private readonly ConcurrentQueue<string> _inactiveBuffer;
private readonly SemaphoreSlim _semaphore;
private bool _isRunning;
private DateTime _lastFlushTime;
private const int MaxMessagesPerBatch = 100; // 每批最大消息数量
private const int FlushIntervalMs = 5000; // 写盘时间间隔,毫秒
public AsyncLogger(string logFilePath)
{
_logFilePath = logFilePath;
_activeBuffer = new ConcurrentQueue<string>();
_inactiveBuffer = new ConcurrentQueue<string>();
_semaphore = new SemaphoreSlim(1, 1);
_isRunning = true;
_lastFlushTime = DateTime.Now;
Task.Run(LoggingLoop);
}
~AsyncLogger()
{
_isRunning = false;
_semaphore.Release();
}
public void Log(LogLevel level, string message, TimestampPrecision precision = TimestampPrecision.Seconds)
{
if (level < LogLevel.Info)
return;
string logMessage = GetTimestamp(precision) + " [" + LogLevelToString(level) + "] " + message;
_activeBuffer.Enqueue(logMessage);
_semaphore.Release();
}
private async Task LoggingLoop()
{
while (_isRunning)
{
await _semaphore.WaitAsync();
if (ShouldFlush())
{
SwapBuffers();
await WriteToDiskAsync();
}
}
// When the logger is stopped, ensure all messages are written.
SwapBuffers();
await WriteToDiskAsync(true);
}
private void SwapBuffers()
{
// Swap the active and inactive buffers.
var temp = _activeBuffer;
_activeBuffer = _inactiveBuffer;
_inactiveBuffer = temp;
}
private async Task WriteToDiskAsync(bool forceFlush = false)
{
if (!_isRunning && !_inactiveBuffer.IsEmpty())
{
forceFlush = true;
}
if (!forceFlush && _inactiveBuffer.IsEmpty())
return;
using (StreamWriter writer = File.AppendText(_logFilePath))
{
string message;
while (_inactiveBuffer.TryDequeue(out message))
{
await writer.WriteLineAsync(message);
}
}
_lastFlushTime = DateTime.Now;
}
private bool ShouldFlush()
{
// Check if we need to flush based on the number of messages or time interval.
return _activeBuffer.Count >= MaxMessagesPerBatch || (DateTime.Now - _lastFlushTime).TotalMilliseconds >= FlushIntervalMs;
}
private string GetTimestamp(TimestampPrecision precision)
{
DateTime now = DateTime.Now;
switch (precision)
{
case TimestampPrecision.Seconds:
return now.ToString("yyyy-MM-dd HH:mm:ss");
case TimestampPrecision.Milliseconds:
return now.ToString("yyyy-MM-dd HH:mm:ss.fff");
case TimestampPrecision.Microseconds:
return now.ToString("yyyy-MM-dd HH:mm:ss.ffffff");
default:
return now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
private string LogLevelToString(LogLevel level)
{
return level.ToString();
}
}
关键方法解释
Log
方法
Log
方法负责接收日志消息并将其添加到活跃缓冲区。当消息被添加后,信号量会被释放,从而可能触发写盘操作。
LoggingLoop
方法
LoggingLoop
方法是一个后台任务,它不断监听信号量的变化。当信号量被释放时,它会检查是否需要进行缓冲区的交换和写盘操作。
SwapBuffers
方法
SwapBuffers
方法用于交换活跃缓冲区和非活跃缓冲区。这允许新的日志消息继续添加到活跃缓冲区,而之前的非活跃缓冲区可以安全地写入磁盘。
WriteToDiskAsync
方法
WriteToDiskAsync
方法负责将非活跃缓冲区中的日志消息写入磁盘。它使用 StreamWriter
来逐条写入消息,并确保每条消息都被正确地写入文件。
ShouldFlush
方法
ShouldFlush
方法检查是否需要进行写盘操作。它基于活跃缓冲区中的消息数量和上次写盘的时间来决定。
使用示例
下面是如何使用 AsyncLogger
的示例:
static void Main(string[] args)
{
AsyncLogger logger = new AsyncLogger("logs/example.log");
logger.Log(LogLevel.Info, "信息消息", TimestampPrecision.Milliseconds);
logger.Log(LogLevel.Warning, "警告消息", TimestampPrecision.Microseconds);
logger.Log(LogLevel.Error, "错误消息", TimestampPrecision.Seconds);
logger.Log(LogLevel.Error, "错误消息,使用缺省的时间戳,秒级精度");
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
总结
本文介绍了一个高效的异步日志记录器的实现。通过使用双缓冲区技术和 ConcurrentQueue
,我们能够有效地避免在写盘过程中阻塞新的日志消息。此外,通过限制每次写盘的消息数量和设定写盘的时间间隔,我们提高了系统的整体性能。希望这个实现能够帮助你在自己的项目中实现高效的日志记录。
这篇文章提供了一个基本的框架,您可以根据需要添加更多的细节和技术背景信息。如果您有任何其他问题或需要进一步的帮助,请随时告诉我。
标签:异步,C#,private,写盘,TimestampPrecision,缓冲区,日志,多线程 From: https://blog.csdn.net/jianglq/article/details/141636424