如何让C#控制台程序等待日志队列处理完成后再终止?
问题描述
我有一个C#日志项目,为提升性能,日志会先加入ConcurrentQueue,由后台异步线程负责写入控制台和文件。但进程快速终止(比如调用Environment.Exit(0))时,队列中剩余的日志来不及处理就会永久丢失。
核心日志处理代码如下:
public static void Info(string content, string? sender = null) => qlog.Enqueue(new LogDetail(content, LogLevel.Information, sender)); ... private static ConcurrentQueue<LogDetail> qlog = new(); public static int RefreshLogTicks { get; set; } private static async Task BackgroundUpdate() { if (!qlog.TryDequeue(out LogDetail _log)) { await Task.Delay(RefreshLogTicks); await Task.Run(BackgroundUpdate); return; } List<string> logs = new(qlog.Count + 1); logs.Add(WriteLog(_log)); while (qlog.TryDequeue(out LogDetail log)) { logs.Add(WriteLog(log)); } ConsoleWrapper.WriteLine(logs); await Task.Delay(RefreshLogTicks); await Task.Run(BackgroundUpdate); }
测试代码:
Console.WriteLine("Hello, "); Log.Info("Logger!", "Hello"); Console.WriteLine("World!"); Environment.Exit(0);
输出仅显示:
Hello, World!
但加入Console.ReadLine()让进程等待后,日志能正常输出:
Console.WriteLine("Hello, "); Log.Info("Logger!", "Hello"); Console.WriteLine("World!"); Console.ReadLine(); Environment.Exit(0);
输出:
Hello, World! 21:28:17 <Info:Hello> Logger!
需求:仅针对正常退出场景(如Environment.Exit()或Application.Exit()),确保进程等待日志队列处理完成后再终止,不希望通过让用户手动调用Stop()方法实现。
解决方案
1. 利用进程退出/程序集卸载事件自动处理剩余日志
通过注册AppDomain.ProcessExit(全.NET版本支持)和AssemblyLoadContext.Unloading(.NET Core 3.0+支持)事件,在进程即将退出时触发剩余日志的处理,并等待后台线程完成。
改造后的日志核心代码如下:
// 新增字段:跟踪退出信号和后台任务 private static readonly TaskCompletionSource<bool> _exitSignal = new(); private static readonly object _lockObj = new(); private static Task? _backgroundLoopTask; // 静态构造函数:启动后台任务并注册退出事件 static Log() { RefreshLogTicks = 100; _backgroundLoopTask = RunBackgroundLogLoop(); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; AssemblyLoadContext.Default.Unloading += OnAssemblyUnload; } // 重构后台逻辑为循环模式,支持监听退出信号 private static async Task RunBackgroundLogLoop() { while (true) { // 同时等待退出信号、队列有日志或延迟超时 var waitForExitOrDelay = Task.WhenAny(_exitSignal.Task, Task.Delay(RefreshLogTicks)); await waitForExitOrDelay; // 收到退出信号:处理所有剩余日志后终止循环 if (_exitSignal.Task.IsCompleted) { ProcessAllRemainingLogs(); break; } // 处理当前队列中的所有日志 ProcessBatchLogs(); } } // 批量处理队列中的日志 private static void ProcessBatchLogs() { List<string> logs = new(); while (qlog.TryDequeue(out LogDetail log)) { logs.Add(WriteLog(log)); } if (logs.Count > 0) { ConsoleWrapper.WriteLine(logs); } } // 处理退出时剩余的所有日志 private static void ProcessAllRemainingLogs() { ProcessBatchLogs(); // 复用批量处理逻辑 } // 进程退出事件处理 private static void OnProcessExit(object? sender, EventArgs e) { TriggerExitAndWait(); } // 程序集卸载事件处理(.NET Core+) private static void OnAssemblyUnload(AssemblyLoadContext ctx) { TriggerExitAndWait(); } // 触发退出信号并等待后台任务完成 private static void TriggerExitAndWait() { lock (_lockObj) { if (!_exitSignal.Task.IsCompleted) { _exitSignal.SetResult(true); } _backgroundLoopTask?.Wait(); } }
2. 关键说明
- 退出事件会在
Environment.Exit()、正常程序结束时触发,但无法拦截Kill()等操作系统级强制终止操作。 - 重构后的循环模式统一管理后台任务,避免了原递归
Task.Run导致的任务无法跟踪问题。 - 确保
WriteLog和ConsoleWrapper.WriteLine是线程安全的,避免退出时出现异常。
原代码日志丢失的原因
原代码通过递归调用Task.Run(BackgroundUpdate)创建多个独立后台任务,进程退出时这些未完成的任务会被直接终止,没有机会处理队列中剩余的日志。新的循环模式可以统一监听退出信号,确保所有日志被处理后再结束。
内容的提问来源于stack exchange,提问作者Yyh
相关产品推荐
相关产品推荐

