Keep .NET console application alive until the termination sequence finishes

I am working on a data acquisition application and I want to ensure that it exits gracefully. That is, it processes all the already collected data, flushes all the (file) buffers to "disk" (persistent memory) and might even uploads the data to the cloud.

So, I wrote (based on this answer) the code below to catch every close event. (It is just a test code.)

Problem: If I use the X in the top-right corner of the console, the program gets terminated after a short delay, even though the termination sequence is still running. (The handler does get called, and it does start to wait for the threads to join but then it gets killed after a while.) If I terminate with Crt+C or Ctr+Break it works as intended; The termination sequence finishes and exits the process.

Question: How can I make the OS wait for my application to terminate instead of killing it off after a short grace period?

#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();
}

1 answer

  • answered 2018-03-11 14:01 CKII

    Since C# 7.1 you can have an async Task Main() method. Using this, you can modify your handler to create a method, and wait on it in the Main method.

    Sidenote: you should use tasks instead of threads wherever you can. Tasks manage your thread better, and they run of the ThreadPool. When you create a new Thread instance, it's assuming it'll be a long running task, and the windows will treat it differently.

    So with that in mind, consider wrapping the TerminateSequence method in a task, instead of a thread, and make that task a member of your class. Now you won't have to wait for it in the context of the handler, you could instead wait for it in the Main method.

    With the rest of your code remaining the same, you could do the following:

    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
    }
    

    If you're not working with C# 7.1 you can still do this, it'll just be a little less elegant. All you need to do is wait for it:

    _finalTask?.Wait();
    

    And that should do it.