大数跨境
0
0

仅 100 行代码!C# 实现高性能多线程日志系统(含批量写入+自动分卷)

仅 100 行代码!C# 实现高性能多线程日志系统(含批量写入+自动分卷) DotNet技术匠
2025-11-12
2
导读:用 100 行代码实现支持多线程、批量写入与文件分块的日志方法。

前言

在上一篇文章中提到高并发的内容,感兴趣的朋友可以阅读C# 工控环境下多线程的高并发处理(附源码)

一、为什么不用现成的日志框架?

提到 .NET 日志组件,大家首先想到的是:

  • Log4Net

  • Enterprise Library Logging

  • ServiceStack.Logging

组件功能强大,但都有一个共同痛点:必须在 app.config 或 web.config 中配置大量 XML,甚至 Log4Net 还要把 SQL 写进配置文件。

对于"作者只是想简单写个日志"的场景,这种重量级配置显得过于繁琐。

因此,作者曾长期使用一个极简方法——仅用 5~6 行核心代码实现基础日志写入,自带异常容错,轻量高效。

File.AppendAllText(logPath, $"{DateTime.Now} {message}\n");

但它在多线程并发写入时会崩溃。

二、多线程下的日志冲突问题

当 100 个线程同时调用日志写入,文件被多个线程争抢,极易引发 IOException 或 文件被占用异常

常见解决方案(但有瓶颈):

  • 使用 lock 或 Monitor.Enter/Exit 加锁

  • 虽然线程安全,但严重阻塞性能,所有日志必须串行写入。

这不是我们想要的"高性能"。

三、重新设计 异步队列 + 批量持久化

设计思路

所有线程只负责"投递"日志到线程安全队列;

单个后台任务从队列中批量读取并写入文件;

自动分块:当日志文件超过 1MB(可配置),自动创建新分卷(如 app(0).logapp(1).log...)。

核心思想:生产者-消费者模型 + 异步批处理

四、三种实现方案对比

方案一:Task + 状态判断(存在竞态风险)

  • 动态创建 Task,判断 IsCompleted 后重启。

  • 问题:高并发下易出现 Task 状态不一致,抛出异常(如"任务已启动")。

  • 不推荐用于生产环境

方案二:长运行 Task + 信号量触发(推荐)

  • 启动一个 LongRunning 的后台 Task;

  • 使用 ManualResetEvent 控制写入时机;

  • 每次有新日志就 Set() 信号,唤醒写入线程;

  • 写完后 Reset(),避免空转。

优点

  • 无锁、低延迟、高吞吐;

  • 避免频繁创建/销毁 Task;

  • 线程安全由 ConcurrentQueue 保障。

static ManualResetEvent pause = new ManualResetEvent(false);

// 后台任务
writeTask = Task.Factory.StartNew(() => {
    while (true) {
        pause.WaitOne(); // 等待日志到来
        pause.Reset();
        BatchWriteLogs(); // 批量写入
    }
}, TaskCreationOptions.LongRunning);

方案三:定时器轮询(简单但低效)

  • 使用 System.Timers.Timer 每秒检查队列;

  • 实现简单,但存在 延迟 和 空轮询开销

  • 需手动控制 Enabled = false/true 防止重入。

适合低频日志场景,不推荐高并发系统。

五、关键代码

1、线程安全队列

static ConcurrentQueue<Tuple<stringstring>> logQueue = 
    new ConcurrentQueue<Tuple<stringstring>>();

无需手动加锁,天然支持多线程 Enqueue/Dequeue。

2、智能日志分卷

  • 按日期生成文件名:MyApp20251111(0).log

  • 单文件超 1MB 自动新建 (1).log(2).log...

  • 使用正则提取序号,确保顺序正确。

3、批量合并写入

  • 将同一路径的日志合并为一次 I/O 操作;

  • 减少磁盘写入次数,提升性能。

完整推荐实现(信号量改进版)

namespace LogTest
{
    publicclassIOExtention
    {
        static ConcurrentQueue<Tuple<stringstring>> logQueue = new ConcurrentQueue<Tuple<stringstring>>();

        static Task writeTask = default(Task);

        static ManualResetEvent pause = new ManualResetEvent(false);

        //Mutex mmm = new Mutex();
        static IOExtention()
        {
            writeTask = new Task((object obj) =>
            {
                while (true)
                {
                    pause.WaitOne();
                    pause.Reset();
                    List<string[]> temp = new List<string[]>();
                    foreach (var logItem in logQueue)
                    {
                        string logPath = logItem.Item1;
                        string logMergeContent = String.Concat(logItem.Item2, Environment.NewLine, "-----------------------------------------------------------", Environment.NewLine);
                        string[] logArr = temp.FirstOrDefault(d => d[0].Equals(logPath));
                        if (logArr != null)
                        {
                            logArr[1] = string.Concat(logArr[1], logMergeContent);
                        }
                        else
                        {
                            logArr = newstring[] { logPath, logMergeContent };
                            temp.Add(logArr);
                        }
                        Tuple<stringstring> val = default(Tuple<stringstring>);
                        logQueue.TryDequeue(out val);
                    }
                    foreach (string[] item in temp)
                    {
                        WriteText(item[0], item[1]);
                    }
                    
                }
            }
            , null
            , TaskCreationOptions.LongRunning);
            writeTask.Start();
        }

        public static void WriteLog(String preFile, String infoData)
        {
            WriteLog(string.Empty, preFile, infoData);
        }

        
        public static void WriteLog(String customDirectory, String preFile, String infoData)
        {
            string logPath = GetLogPath(customDirectory, preFile);
            string logContent = String.Concat(DateTime.Now, " ", infoData);
            logQueue.Enqueue(new Tuple<stringstring>(logPath, logContent));
            pause.Set();
        }

        private static string GetLogPath(String customDirectory, String preFile)
        {
            string newFilePath = string.Empty;
            String logDir = string.IsNullOrEmpty(customDirectory) ? Path.Combine(Environment.CurrentDirectory, "logs") : customDirectory;
            if (!Directory.Exists(logDir))
            {
                Directory.CreateDirectory(logDir);
            }
            string extension = ".log";
            string fileNameNotExt = String.Concat(preFile, DateTime.Now.ToString("yyyyMMdd"));
            String fileName = String.Concat(fileNameNotExt, extension);
            string fileNamePattern = string.Concat(fileNameNotExt, "(*)", extension);
            List<string> filePaths = Directory.GetFiles(logDir, fileNamePattern, SearchOption.TopDirectoryOnly).ToList();

            if (filePaths.Count > 0)
            {
                int fileMaxLen = filePaths.Max(d => d.Length);
                string lastFilePath = filePaths.Where(d => d.Length == fileMaxLen).OrderByDescending(d => d).FirstOrDefault();
                if (new FileInfo(lastFilePath).Length > 1 * 1024 * 1024 * 1024)
                {
                    string no = new Regex(@"(?is)(?<=\()(.*)(?=\))").Match(Path.GetFileName(lastFilePath)).Value;
                    int tempno = 0;
                    bool parse = int.TryParse(no, out tempno);
                    string formatno = String.Format("({0})", parse ? (tempno + 1) : tempno);
                    string newFileName = String.Concat(fileNameNotExt, formatno, extension);
                    newFilePath = Path.Combine(logDir, newFileName);
                }
                else
                {
                    newFilePath = lastFilePath;
                }
            }
            else
            {
                string newFileName = String.Concat(fileNameNotExt, String.Format("({0})"0), extension);
                newFilePath = Path.Combine(logDir, newFileName);
            }
            return newFilePath;
        }

        private static void WriteText(string logPath, string logContent)
        {
            try
            {
                if (!File.Exists(logPath))
                {
                    File.CreateText(logPath).Close();
                }
                StreamWriter sw = File.AppendText(logPath);
                sw.Write(logContent);
                sw.Close();
            }
            catch (Exception ex)
            {

            }
            finally
            {

            }
        }
    }
}

总结

方案
优点
缺点
适用场景
简单写入
极简
多线程崩溃
单线程调试
Lock 加锁
线程安全
性能瓶颈
低并发
Task + 状态判断
异步
竞态风险
 不推荐
信号量 + 长任务
高性能、低延迟、安全
稍复杂
 生产环境推荐
定时器轮询
简单
延迟高、资源浪费
低频日志

最终建议:对于需要轻量、无配置、高性能的日志场景,自研基于 ConcurrentQueue + 后台批处理 的组件是不错的选择。

思考

  • 是否可加入内存缓冲上限防止 OOM?

  • 是否支持日志级别(Info/Debug/Error)?

  • 是否可对接远程日志服务(如 ELK)?

欢迎在评论区留言交流!

关键词

#日志组件#多线程日志#ConcurrentQueue#批量写入#文件分块、.#高性能日志#无配置日志#Task异步日志#AutoResetEvent#ManualResetEvent、System.Timers.Timer、#多线程日志组件#文件分块写入#线程安全队列#工业自动化#批量日志处理#断点续传#信号量同步

最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

作者:Sam Xiao

出处:cnblogs.com/xcj26/p/6037808.html
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!



END



方便大家交流、资源共享和共同成长
纯技术交流群,需要加入的小伙伴请扫码,并备注加群



推荐阅读






C# 工业常用的控件库
C# 工控环境下多线程的高并发处理(附源码)
C# 打造轻量级上位机,高效打通 MES 与视觉检测系统
基于 WinForm GDI+ 的高性能矢量画布组件
WPF + MVVM 重塑康耐视 VisionPro 调试工具
WinForm + MVP 架构联合:打造超实用图书管理系统
WPF 高颜值工业上位机快速开发框架 
WinForm +SQLite 开发的高效PLC数据采集系统
C# 开发的串口固件传输工具 支持OTA升级
C# 实现海康相机 + PLC + 数据库的工业通信集成
WinForm 无线环境监控上位机系统设计与实现
工业自动化实战:C# 实现 Basler 相机图像采集系统
C# 基于机器视觉的液体颜色识别系统

C# 实现 GB28181标准与流媒体推流的完整指南

基于 .NET + Vue 3 的线路图绘制系统实战(含源码)

WinForm 下基于策略与工厂模式的 PLC 数据采集与监控系统

C# 开发工业级温湿度上位机:实时采集与存储

面向工业自动化的 WPF PLC 风格上位机开发框架

C# 写的一个开源免费的OPC UA网关,支持西门子PLC

WinForm + FFmpeg 开发的轻量级视频压缩工具

.NET 8 + Avalonia 跨平台简易校园信息管理系统的开发实战

Windows 服务可视化管理器:安装、启停、定时全搞定

C# + WPF + SuperSocket 开发面向工业自动化的 MES 系统

告别服务宕机,C# 看门狗守护你的 WinForm 与 Windows 服务

.NET 一款高效跨平台的自动更新工具(差异更新+热修复+自动升级)

面向工厂自动化的智能语音播报方案(基于.NET Windows服务)

基于 WPF + Prism 的工业自动化监控系统开源实践

工业自动化UI太难做?WPF 这套工业级控件方案真香(附源码)

工业自动化 WPF + Halcon 的模块化机器视觉解决方案

C# 开源视觉与运动控制集成平台,模块化设计赋能工业自动化

开源福利!八款 WPF + HandyControl 工业管理系统源码全公开

WinForm + Win32 API 自定义无边框窗口实战(工业软件必备)

WPF + MVVM架构的轻量级视频播放器实现

基于 HslCommunication 的多端同步PLC远程监控系统

C# + Vue 面向工业场景的实时数据采集与监控平台

WinForm 数据采集实战:从串口通信到MES对接的轻量化解决方案

一个拒绝过度设计的 .NET 快速开发框架:开箱即用,专注"干活"

C# 工业视觉全流程实战:模板匹配、胶钉定位与下位机通信
WPF 通信控制台:功能丰富、界面美观的上位机开发实战
拿来就用!一个基于 .NET 6 + WPF 的开源数据大屏模板

WinForm + SunnyUI  与 MQTTnet 实现智能可视化的火警联动大屏系统

.NET 9 + WPF + Halcon 构建工业视觉流程框架:从架构设计到落地实践

WinForm 高分屏适配难题?一款强大的控件自适应缩放工具

基于 .NET 6 + OpenCVSharp 的跨平台工业视觉图像分析工具
WinForm 框架下的工控领域视觉检测
基于 .NET 8 + React 的轻量高效库存管理系统(前后端分离)
WPF 实时工业监控大屏:ModBus协议集成与无边框动态可视化方案
图形化操作 Windows 服务?这个开源小工具做到了
.NET 9.0 一个可复用 WPF 界面框架
手把手教会设计 WinForm 高DPI兼容程序,告别字体模糊与控件乱飞

WPF + MVVM 自助式检验报告打印机的多框架实现

为什么 .NET 内存占用非常大?

C# 部署 Yolov8 全攻略:OpenVINO 与 TensorRT  双引擎加速
WPF 一款通用的嵌入式测控上位机(灵活配置免重复)
全栈 .NET 低代码引擎:权限、工作流、API动态生成,开源即用
一款基于 .NET 的轻量级 ERP 进销存系统:扫码入库、订单变标签,直达发货
.NET 8 + Vue 3 的智能工厂 MES 快速开发框架:设备监控、数据大屏全覆盖
.NET 9 + React 基于 DDD架构的动态路由 + RBAC权限实战
基于 SunnyUI 的企业级 WinForm 快速开发框架,开箱即用!
免硬件方案!基于.NET 的摄像头扫码工具(支持回车/连续扫描)
工业级 MES 系统开发 WPF + MVVM 从入门到实战(全源码/收藏版)


觉得有收获?不妨分享让更多人受益

关注「DotNet技术匠」,共同提升技术实力


收藏
点赞
分享
在看

【声明】内容源于网络
0
0
DotNet技术匠
「DotNet技术匠」聚焦.NET核心,分享深度干货、实战技巧、最新资讯、优质资源,助你领跑技术赛道,赋能开发者成长。
内容 1715
粉丝 0
DotNet技术匠 「DotNet技术匠」聚焦.NET核心,分享深度干货、实战技巧、最新资讯、优质资源,助你领跑技术赛道,赋能开发者成长。
总阅读53
粉丝0
内容1.7k