使用并发队列写入文件时出现日志丢失问题
并发日志写入丢失问题分析与修复方案
你的日志丢失问题源于代码里几个关键设计缺陷,逐个拆解:
1. async void 导致任务失控
LogToFile 和 AppendMessage 都使用了 async void,这种写法在非UI场景下会引发两个致命问题:
- 调用方无法等待异步任务完成,程序退出时可能还有大量日志写入任务未执行,直接被系统终止,导致日志丢失。
- 异步过程中抛出的异常无法被上层捕获,出现静默失败你完全察觉不到。
2. 无限制并发写入引发冲突
你现在每收到一条日志就启动一个Task.Run去写文件,相当于同时有上百个线程操作同一个文件。即便用了FileMode.Append和FileShare.Write,异步写入的情况下,多个WriteAsync操作会出现执行交错——操作系统的追加模式无法保证多异步写入的原子性,最终导致部分写入被覆盖或丢失。
3. CallQueue 未起到队列作用
当前的CallQueue只是初始化了一个ConcurrentQueue,但日志写入逻辑完全没用到这个队列缓存消息,直接触发写入,队列形同虚设。另外CurrentCalls++不是原子操作,多线程下会出现计数错误,完全不可靠。
修复方案
改成单线程消费日志队列的模式,所有日志先入队,由一个后台线程负责串行写入文件,从根源避免并发写入冲突:
修复后的代码示例
public static class Logging { // 用BlockingCollection实现线程安全的日志队列,自动处理阻塞等待 private static readonly BlockingCollection<string> _logQueue = new BlockingCollection<string>(); // 后台写入任务,确保程序运行期间持续处理队列 private static readonly Task _writeTask; static Logging() { // 启动后台线程处理日志写入 _writeTask = Task.Run(() => ProcessLogQueue()); } // 对外提供的日志方法,改为async Task让调用方可按需等待 public static async Task LogToFile(LogType type, string message) { // 标准化日志格式,加入时间戳和类型 var logMessage = $"[{type}] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}"; // 异步入队,避免阻塞调用方 await Task.Run(() => _logQueue.Add(logMessage)); } // 后台处理队列的方法,串行写入文件 private static void ProcessLogQueue() { try { // 遍历队列,直到队列被标记为完成 foreach (var message in _logQueue.GetConsumingEnumerable()) { if (string.IsNullOrEmpty(message)) continue; // 单线程下用同步写入即可,避免异步额外开销 using (var writer = new StreamWriter(FullPath, true, Encoding.ASCII)) { writer.WriteLine(message); } } } catch (Exception ex) { Console.WriteLine($"日志写入失败: {ex.ToString()}"); } } // 程序退出时调用,确保队列中所有日志都被写入 public static void Shutdown() { _logQueue.CompleteAdding(); _writeTask.Wait(); } // 替换为你的实际日志文件路径 private static string FullPath => @"C:\logs\app.log"; } // 你的LogType枚举 public enum LogType { Info, Error, Warning }
关键改进点
- 单线程串行写入:所有日志先进入线程安全的
BlockingCollection队列,由一个后台线程负责写入,彻底避免并发写入的冲突和覆盖。 - 移除
async void:LogToFile改为async Task,调用方可按需等待日志入队完成;后台处理用同步写入,逻辑更简单可靠。 - 程序退出处理:新增
Shutdown方法,在程序退出前调用,确保队列中所有日志都被写入文件,不会因程序终止丢失。 - 日志格式标准化:加入时间戳和日志类型,方便后续问题排查。
调用示例
// 循环写入700条日志 for (int n = 0; n < 700; n++) { await Logging.LogToFile(LogType.Info, $"Number {n}"); } // 程序退出前务必调用 Logging.Shutdown();
内容的提问来源于stack exchange,提问作者DPunOP
相关产品推荐
相关产品推荐

