首页 > 编程语言 >C#环境下支持多线程的异步日志记录器的设计与实现

C#环境下支持多线程的异步日志记录器的设计与实现

时间:2024-08-28 12:51:49浏览次数:14  
标签:异步 C# private 写盘 TimestampPrecision 缓冲区 日志 多线程

上篇博文提供了c++版异步日志类,本文提供同样功能的C#版的异步日志管理类。

C#环境下支持多线程的异步日志记录器的设计与实现

在现代应用程序开发中,日志记录是一项至关重要的任务,它帮助开发者追踪程序的运行情况,调试问题,并进行性能监控。特别是在高并发场景下,传统的同步日志记录方式可能会成为性能瓶颈。因此,设计一个高效、可扩展的异步日志记录器变得尤为重要。

本文将介绍一个简单的异步日志记录器类的实现,该记录器采用双缓冲区技术来提高效率,并利用 C# 的 ConcurrentQueueSemaphoreSlim 等工具来保证线程安全和高性能。

设计理念

我们的目标是创建一个异步日志记录器,它可以:

  • 收集来自多个线程的日志消息,并将它们异步地写入磁盘。
  • 使用双缓冲区技术来避免在写盘过程中阻塞新的日志消息。
  • 限制每次写盘的日志数量,以减少磁盘 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

相关文章

  • C语言--运算符2
    目录位运算符1、&按位与2、|按位或3、~按位取反4、^按位异或5、位移(1)右移>>(2)左移<<位运算符例:128十进制转二进制正数在内存中以原码形式存放,负数在内存中以补码形式存放正数的原码=反码=补码原码:将一个整数,转换成二进制,就是其原码。如64的原码为:0100 00......
  • 109.微软邮箱强制要求使用MS Authenticator手机APP但中国没有GooglePlay的处理办法
    109.微软邮箱强制要求使用MSAuthenticator手机APP但中国没有GooglePlay的处理办法  背景: 微软邮箱强制用户使用它的Authenticator手机验证器APP(只能跳过3次), 而大部分中国用户手机上是没有谷歌框架和GooglePlay的,所以导致很多用户无法使用微软企业邮箱微软自己也发现了......
  • QOJ #9222. Price Combo
    题面传送门假设只有一维,那么肯定是最奇数大的买,第偶数大的不买,以此类推。但是现在有两维,如果只对一维做的话,另一维的顺序是不固定的,不好处理。我们考虑发掘一点性质,假设对于两个物品\((a_i,b_i),(a_j,b_j)\),如果满足\(a_i\leqa_j,b_i\geqa_j\),则肯定不会\(i\)用\(b\)买......
  • 【开源指标管理中台dcluster】在线体验环境上线啦
    大家好,新一代指标中台dcluster正式开源了,它是一款致力于开发从数据集成到数据开发治理再到数据智能分析的一站式服务的指标中台开源项目。通过深入研究优秀的开源项目,集成到dcluster中。目前已经集成dolphinscheduler实现数据开发和数据同步集成,集成supersonic实现了基于指标管......
  • 【音视频通话】使用asp.net core 8+vue3 实现高效音视频通话
    引言在三年前,写智能小车的时候,当时小车上有一个摄像头需要采集,实现推拉流的操作,技术选型当时第一版用的是nginx的rtmp的推拉流,服务器的配置环境是centos,2H4G3M的一个配置,nginx的rtmp的延迟是20秒,超慢,后来研究了SRS以及ZLMediaKit这两个开源的推拉流服务器,没记错的话,两个......
  • kafka如何合理设置broker、partition、consumer数量
    目录1.broker的数量最好大于等于partition数量2.consumer数量最好和partition数量一致3.总结1.broker的数量最好大于等于partition数量一个partition最好对应一个硬盘,这样能最大限度发挥顺序写的优势。一个broker如果对应多个partition,需要随机分发,顺序IO会退化成随机IO。实......
  • 毕业设计——基于IPSec VPN的高可靠性中学校园网络设计与实现
    ​​​​​​​目录文章目录摘要1前言1.1研究背景 1.2国内外研究现状 1.3设计方法与思路 1.3.1设计方法2.需求分析2.1用户及网络需求分析2.1.1网络需求分析2.1.2用户信息需求2.1.3业务需求分析2.1.4应用需求分析2.2可行性分析 2.3现状分析 ......
  • 一次搞懂数据大屏适配方案 (vw vh、rem、scale)
    当接到可视化大屏需求时,你是否会有以下疑问......
  • SciTech-Mathmatics-Probability+Statistics: Understanding the $\large Null\ and
    NullHypothesisforLinearRegressionLinearregressionisatechniquewecanusetounderstandtherelationshipbetweenoneormorepredictorvariablesandaresponsevariable.SimpleLinearRegressionIfweonlyhaveonepredictorvariableandonerespo......
  • phpinclude-labs做题记录
    Level1file协议payload:?wrappers=/flagLevel2data协议去包含data协议中的内容其实相当于进行了一次远程包含,所以data协议的利用条件需要php.ini中开启allow_url_fopen和allow_url_includeGET:?wrappers=,然后POST:helloctf=system('cat/flag');Level3data......