C#日志写入遇文件被占用异常,求线程锁或队列实现方案
解决方案:线程锁与等待队列实现无冲突日志写入
一、线程锁方式(简单直接,适合低并发场景)
问题根源是多线程同时访问同一个日志文件,用lock关键字可确保同一时间只有一个线程写入文件。锁对象必须声明为静态且只读,避免不同实例创建不同锁导致同步失效。
修改后的代码示例:
// 静态只读锁对象,确保所有线程共享同一锁实例 private static readonly object _logLock = new object(); public void WriteLog(string webService, string eventName, string transactionId, string debuglog) { DateTime time = DateTime.Now; time = time.Minute > 30 ? time.Date.AddHours(time.Hour).AddMinutes(30) : time.Date.AddHours(time.Hour).AddMinutes(0); var filePath = $"C:/temp/DebugLog/{webService}/{time:yyyyMMddHHmss}_Logs/"; var fullFileName = Path.Combine(filePath, $"{time:yyyyMMddHHmss}.txt"); if (!Directory.Exists(filePath)) { Directory.CreateDirectory(filePath); } // 加锁后执行文件写入,避免多线程冲突 lock (_logLock) { try { using (StreamWriter w = File.AppendText(fullFileName)) { w.WriteLine($"{Guid.NewGuid():N}; {DateTime.Now:yyyyMMddHHmssms}; {webService}; {eventName}; {DateTime.Now}; {transactionId}; {debuglog}"); // using块会自动释放StreamWriter资源,无需手动调用Close() } } catch (Exception ex) { // 可在此添加异常处理逻辑,比如记录到系统事件日志 // EventLog.WriteEntry("日志写入失败", ex.Message, EventLogEntryType.Error); } } }
核心注意点:
- 锁对象必须是静态的,否则不同类实例会生成独立锁,无法起到同步作用
- 使用
Path.Combine拼接路径,避免手动拼接导致的分隔符错误 using块自动管理资源生命周期,无需手动关闭流
二、等待队列方式(适合高并发场景,异步解耦)
高并发场景下,线程锁会导致线程阻塞等待,影响业务性能。可采用生产-消费模式:用线程安全队列缓存日志内容,单独启一个线程负责写入文件,实现业务逻辑与日志操作解耦。
示例代码:
// 线程安全的日志队列,用于缓存待写入的日志数据 private static readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>(); // 标记队列处理线程的运行状态,避免重复启动 private static bool _isProcessingQueue = false; // 控制队列处理线程启动的锁对象 private static readonly object _queueLock = new object(); public void WriteLog(string webService, string eventName, string transactionId, string debuglog) { DateTime time = DateTime.Now; time = time.Minute > 30 ? time.Date.AddHours(time.Hour).AddMinutes(30) : time.Date.AddHours(time.Hour).AddMinutes(0); var logContent = $"{Guid.NewGuid():N}; {DateTime.Now:yyyyMMddHHmssms}; {webService}; {eventName}; {DateTime.Now}; {transactionId}; {debuglog}"; var filePath = $"C:/temp/DebugLog/{webService}/{time:yyyyMMddHHmss}_Logs/"; var fullFileName = Path.Combine(filePath, $"{time:yyyyMMddHHmss}.txt"); // 将日志内容和目标文件路径序列化后入队 _logQueue.Enqueue(JsonConvert.SerializeObject(new { FilePath = fullFileName, Content = logContent })); // 确保仅启动一个队列处理线程 lock (_queueLock) { if (!_isProcessingQueue) { _isProcessingQueue = true; Task.Run(() => ProcessLogQueue()); } } } private void ProcessLogQueue() { while (_logQueue.TryDequeue(out string logItem)) { try { var logData = JsonConvert.DeserializeObject<dynamic>(logItem); string fullFileName = logData.FilePath; string content = logData.Content; var directory = Path.GetDirectoryName(fullFileName); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 写入文件时可针对单个文件加锁,避免同一文件的并发写入冲突 lock (_logLock) { using (StreamWriter w = File.AppendText(fullFileName)) { w.WriteLine(content); } } } catch (Exception ex) { // 异常处理:可将失败的日志重新入队重试,或记录到错误日志 // _logQueue.Enqueue(logItem); } } // 队列处理完毕,重置运行状态标记 lock (_queueLock) { _isProcessingQueue = false; } }
核心注意点:
- 用
ConcurrentQueue实现线程安全的队列操作,无需手动加锁 - 消费线程仅启动一个,避免资源浪费
- 支持失败日志重试,提升日志写入可靠性
总结
- 低并发场景:优先使用线程锁,实现简单、性能开销小
- 高并发场景:采用等待队列+异步消费,解耦业务与日志操作,避免阻塞业务线程
内容的提问来源于stack exchange,提问作者Ah Chua
相关产品推荐
相关产品推荐

