You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 11:05:31