.NET获取应用退出 CancellationToken

Intro

当前我们的应用原来越多的使用异步方法,而异步方法通常会有一个 CancellationToken 的参数以及时取消我们的异步操作,那我们如何获取一个应用退出的 CancellationToken 呢,我们只需要使用一个 CancellationTokenSource 在应用退出的时候将其 Cancel 即可.

Cancel C exit

对于简单的 Ctrl+C 退出的我们可以通过 Console.CancelKeyPress 事件来处理,实现如下:

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

我们想要注册退出事件的话可以通过这个 CancellationToken 来 Register

我们来测试一下,测试代码如下:

var exitToken = ConsoleHelper.GetExitToken();
exitToken.Register(() => Console.WriteLine(@"Console exiting"));
Console.ReadLine();

.NET获取应用退出 CancellationToken

我们稍微改造一下测试代码,在回调里增加三秒的等待

var exitToken = ConsoleHelper.GetExitToken();
exitToken.Register(() => 
{
    Console.WriteLine(@"Console exiting");
    Thread.Sleep(3000);
    Console.WriteLine(@"Console exited");
});
Console.WriteLine("starting");
Console.ReadLine();

.NET获取应用退出 CancellationToken

从输出结果可以看到我们的 Console exited 并没有被打印出来

如果注意的话会发现 CancelKeyPress 的事件参数 ConsoleCancelEventArgs 里有一个 Cancel 的属性,默认值是 false

我们如果设置为 true 则会取消结束进程,asp.net core 也是借助于此来实现 graceful shutdown

我们再来改造一下 exitToken 再事件处理的时候设置为 true

args.Cancel = true;

改造一下示例:

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

.NET获取应用退出 CancellationToken

我们再将 Cancel 的设置去掉试一下

.NET获取应用退出 CancellationToken

 

不知道你是否看出来其中区别,设置为 true 的时候 block 到最后的 Console.ReadLine() 了,不设置的时候进程终止了,可以根据自己需要进行调整

Process exits

前面我们只处理了 Console.CancelKeyPress 的事件,实际进程有可能会被外部强制终止,这些情况基本不会被捕获,需要额外的设置才可以

我们可以在 dotnet core 的 Hosting 部分 ConsoleLifetime 的代码里找到注册应用退出事件的方法

前段时间也看到石头哥发的他们测试的各种应用退出的测试,这里借用一下石头哥的测试结果:

.NET获取应用退出 CancellationToken

 

感兴趣的可以参考:https://newlifex.com/blood/elegant_exit,也找了一下他们应用退出的注册,大部分是一样的

有一个 SIGQUIT 在 ConsoleLifetime 里有注册,石头哥他们库里没注册,给他们提了一个 PR

https://github.com/NewLifeX/X/pull/128

用到的所有的注册方法如下:

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

实现代码如下:

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 使用 Lazy 做懒初始化,用不到的时候就不创建,通过 _exited 和 _exitLock 避免 exit 方法多次执行,如果 CancellationTokenSource 没有创建的话,也就无需处理,如果创建了,就触发 CancellationToken 并在触发之后 Dispose

Sample

使用起来和前面 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);
}

输出结果如下:

.NET获取应用退出 CancellationToken