Get application exit CancellationToken
Intro
Currently, our applications use more and more asynchronous methods. Asynchronous methods usually have a CancellationToken
parameter to cancel our asynchronous operation in time. So how do we get the CancellationToken
CancellationTokenSource
to cancel it when the application exits.
Cancel C exit
For a simple Ctrl + C exit, we can handle it through the Console.CancelKeyPress
event, as follows:
public static class ConsoleHelper { static ConsoleHelper() { Console.CancelKeyPress + = (sender, args) => { CancellationTokenSource.Cancel(false); }; } public static CancellationToken GetExitToken() { return CancellationTokenSource.Token; } private static readonly CancellationTokenSource CancellationTokenSource = new(); }
If we want to register the exit event, we can use this CancellationToken
to Register
Let's test it. The test code is as follows:
var exitToken = ConsoleHelper.GetExitToken(); exitToken.Register(() => Console.WriteLine(@"Console exiting")); Console.ReadLine();
Let’s slightly modify the test code and add a three-second wait in the callback.
var exitToken = ConsoleHelper.GetExitToken(); exitToken.Register(() => { Console.WriteLine(@"Console exiting"); Thread. Sleep(3000); Console.WriteLine(@"Console exited"); }); Console.WriteLine("starting"); Console.ReadLine();
From the output, we can see that our Console exited
was not printed.
If you pay attention, you will find that the event parameter ConsoleCancelEventArgs
of CancelKeyPress
has an attribute of Cancel
, and the default value is false
If we set it to true
, the end process will be canceled. ASP.NET Core also uses this to achieve graceful shutdown.
Let's modify the exitToken and set it to true
when the event is processed
args.Cancel = true;
Modify the example:
var exitToken = ConsoleHelper. GetExitToken(); exitToken.Register(() => { Console.WriteLine(@"Console exiting"); Thread. Sleep(3000); Console.WriteLine(@"Console exited"); }); Console.WriteLine("starting"); Console. ReadLine(); Console.WriteLine("exiting"); Console.ReadLine();
Let's remove the setting of Cancel
and try again
I don’t know if you can see the difference. When it is set to true
, it blocks to the last Console.ReadLine()
. When it is not set, the process terminates. You can adjust it according to your own needs. need to be adjusted
Process exits
Previously we only processed the Console.CancelKeyPress
event. The actual process may be forcibly terminated externally. These situations will basically not be captured and require additional settings.
We can find the method of registering the application exit event in the code of ConsoleLifetime
in the Hosting part of dotnet core
Some time ago, I also saw Brother Shitou’s test of the exit of various applications they tested. Here I borrow the test results of Brother Shitou:
If you are interested, you can refer to: https://newlifex.com/blood/elegant_exit, and also searched for their application exit registration, most of them are the same
There is a SIGQUIT
that is registered in ConsoleLifetime
, but Shitou didn’t register it in their library, so I submitted a PR to them
https://github.com/NewLifeX/X/pull/128
All registration methods used are as follows:
static void InvokeExitHandler(object? sender, EventArgs? args); // https://github.com/NewLifeX/X/blob/e65dfa0998ec393804f3f793f333c237110d890e/NewLife.Core/Model/Host.cs#L61 // https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.notnetcoreapp.cs AppDomain.CurrentDomain.ProcessExit += InvokeExitHandler; Console.CancelKeyPress + = InvokeExitHandler; #if NETCOREAPP System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += ctx => InvokeExitHandler(ctx, null); #endif #if NET6_0_OR_GREATER // https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx => InvokeExitHandler(ctx, null)); PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => InvokeExitHandler(ctx, null)); PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => InvokeExitHandler(ctx, null)); #endif
https://github.com/WeihanLi/WeihanLi.Common/blob/578c5ba80bad9b8073ae6dec3403f884a7ab4e84/src/WeihanLi.Common/Helpers/InvokeHelper.cs#L12
ExitToken
The implementation code is as follows:
private static readonly object _exitLock = new(); private static volatile bool _exited; private static readonly Lazy<CancellationTokenSource> LazyCancellationTokenSource = new(); public static CancellationToken GetExitToken() => LazyCancellationTokenSource.Value.Token; private static void InvokeExitHandler(object? sender, EventArgs? args) { if (_exited) return; lock (_exitLock) { if (_exited) return; Debug.WriteLine("exiting..."); if (LazyCancellationTokenSource.IsValueCreated) { LazyCancellationTokenSource.Value.Cancel(false); LazyCancellationTokenSource.Value.Dispose(); } Debug.WriteLine("exited"); _exited = true; } }
CancellationTokenSource
uses Lazy
for lazy initialization and does not create it when it is not needed. Use _exited
and _exitLock
to avoid exit. The method is executed multiple times. If CancellationTokenSource
is not created, there is no need to process it. If it is created, trigger CancellationToken
and Dispose
after triggering
Sample
The usage is similar to the previous ConsoleHelper.GetExitToken()
var exitToken = InvokeHelper.GetExitToken(); exitToken.Register(() => { Console.WriteLine(@"Exiting"); Thread.Sleep(3000); Console.WriteLine(@"Exited"); }); while(!exitToken.IsCancellationRequested) { System.Console.WriteLine(DateTimeOffset.Now); await Task.Delay(1000); }
The output is as follows:
References
-
https://newlifex.com/blood/elegant_exit
-
https://github.com/NewLifeX/X/blob/e65dfa0998ec393804f3f793f333c237110d890e/NewLife.Core/Model/Host.cs#L61
-
https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.notnetcoreapp.cs
-
https://github.com/dotnet/runtime/blob/940b332ad04e58862febe019788a5b21e266ea10/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs
-
https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html
-
https://github.com/NewLifeX/X/pull/128
-
https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Helpers/InvokeHelper.cs