保持.NET控制台应用程序一直运行,直到终止序列完成。

3
我正在开发一个数据采集应用程序,希望确保程序能够优雅地退出。也就是说,程序需要处理所有已经收集到的数据,将所有(文件)缓冲区刷新到“磁盘”(持久内存)中,并且甚至还可能将数据上传到云端。
我根据这个答案编写了下面的代码来捕获每个关闭事件。(它只是一个测试代码。)
问题在于:如果我使用控制台右上角的 X 关闭程序,虽然终止序列仍在运行,但程序会在短暂延迟后被终止。(处理程序确实被调用,并开始等待线程加入,但之后还是会被杀掉。)如果我使用 Crt+C 或 Ctr+Break 终止程序,它将按预期工作;终止序列完成并退出该进程。
问题是:如何让操作系统等待我的应用程序终止,而不是在短暂的优雅期后杀死它?
#region Trap application termination
[DllImport("Kernel32")]
private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

private delegate bool EventHandler(CtrlType sig);
static EventHandler _handler;

enum CtrlType
{
    CTRL_C_EVENT = 0,
    CTRL_BREAK_EVENT = 1,
    CTRL_CLOSE_EVENT = 2,
    CTRL_LOGOFF_EVENT = 5,
    CTRL_SHUTDOWN_EVENT = 6
}

private static bool Handler(CtrlType sig, List<Thread> threads, List<Task> tasks, CancellationTokenSource cancellationRequest)
{
    //starts new foregeound thread, so the process doesn't terminate when all the cancelled threads end
    Thread closer = new Thread(() => terminationSequence(threads, tasks, cancellationRequest));
    closer.IsBackground = false;
    closer.Start();

    closer.Join();  //wait for the termination sequence to finish

    return true; //just to be pretty; this never runs (obviously)
}
private static void terminationSequence(List<Thread> threads, List<Task> tasks, CancellationTokenSource cancellationRequest)
{
    cancellationRequest.Cancel(); //sends cancellation requests to all threads and tasks

    //wait for all the tasks to meet the cancellation request
    foreach (Task task in tasks)
    {
        task.Wait();
    }

    //wait for all the treads to meet the cancellation request
    foreach (Thread thread in threads)
    {
        thread.Join();
    }
    /*maybe do some additional work*/
    //simulate work being done
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("Spinning");
    while (stopwatch.Elapsed.Seconds < 30)
    {
        if (stopwatch.Elapsed.Seconds % 2 == 0)
        {
            Console.Clear();
            Console.WriteLine("Elapsed Time: {0}m {1}s", stopwatch.Elapsed.Minutes, stopwatch.Elapsed.Seconds);
        }
        Thread.SpinWait(10000);
    }

    Environment.Exit(0); //exit the process
}
#endregion

static void Main(string[] args)
{
    CancellationTokenSource cancellationRequest = new CancellationTokenSource();    //cancellation signal to all threads and tasks
    List<Thread> threads = new List<Thread>(); //list of threads

    //specifys termination handler
    _handler += new EventHandler((type) => Handler(type, threads, new List<Task>(), cancellationRequest));
    SetConsoleCtrlHandler(_handler, true);

    //creating a new thread
    Thread t = new Thread(() => logic(cancellationRequest.Token));
    threads.Add(t);
    t.Start();
}

可能是如何阻止C#控制台应用程序自动关闭?的重复问题。 - Rosdi Kasim
7
很抱歉,像您这样将问题标记为与实际问题毫无关系的重复问题的人正在彻底破坏这个网站... - Cerike
也许这个SO帖子可以给你一些提示。看起来操作系统给你的最大时间是5秒钟来完成所有的清理工作。 - Thangadurai
1
@Thangadurai,该特定超时由.NET运行时控制。如果他使用win32 API控制台方法,则超时为30秒。如果他决定上传数据,两者都不是很好。我正在建议编写一个Windows服务,这应该给他完全的控制权,但“Task”答案更好。 - McGuireV10
死锁是这样的代码非常可能出现的结果,当用户强制终止时,他从不关心您的代码可能正在做什么。很有可能在最糟糕的时候发生,这是他决定结束它的基本原因。这很难调试,操作系统根本没有足够的时间让您找出为什么线程没有响应取消请求。必须用Trace.WriteLine()的方式来解决。 - Hans Passant
显示剩余11条评论
1个回答

2
自C# 7.1版本开始,您可以使用async Task Main()方法。通过这种方式,您可以修改处理程序以创建一个方法,并在Main方法中等待它。
顺便提一下:无论何时都应该使用任务而不是线程。任务可以更好地管理您的线程,并在线程池上运行。当您创建新的线程实例时,它假定它将是一个长时间运行的任务,而Windows会以不同的方式对待它。
因此,在考虑这一点时,请将TerminateSequence方法封装在一个任务中,而不是一个线程,并使该任务成为您的类的成员。现在,您不必在处理程序的上下文中等待它,而可以在Main方法中等待它。
在代码的其余部分保持不变的情况下,您可以执行以下操作:
private Task _finalTask;

private static bool Handler(CtrlType sig, List<Thread> threads, List<Task> tasks, CancellationTokenSource cancellationRequest)
{
    //starts new foregeound thread, so the process doesn't terminate when all the cancelled threads end
    _finalTask = Task.Run(() => terminationSequence(threads, tasks, cancellationRequest));
}

// ...

static async Task Main(string[] args)
{
    // ...

    // Wait for the termination process
    if(_finalProcess != null)
        await _finalTask
}

如果您没有使用C# 7.1,您仍然可以这样做,只是不太优雅。您所需要做的就是等待它:
_finalTask?.Wait();

这应该就可以了。


2
我喜欢这个答案,但我建议原帖作者在边缘情况下测试行为,例如用户注销或机器关闭。我知道在旧的Win32控制台API中,这些情况处理方式非常不同。 - McGuireV10
2
你应该使用 _finalTask?.GetAwaiter().GetResult();。请参考 https://dev59.com/PVoV5IYBdhLWcg3wTdQc 了解原因。 - Cameron MacFarland
@CKII 顺便说一下:我确实尝试了你的解决方案,但它并不起作用。为什么会这样呢?(也许我错过了什么。)但是,你只是以稍微不同的方式做了与我相同的事情,因此它具有相同的问题;操作系统/.NET框架在它完成之前就将其终止了。 - Cerike
2
我不认为这会有所帮助。无论你使用任务、线程还是其他什么,操作系统仍然会以同样的方式终止进程。 - Evk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接