获取应用退出 CancellationToken

获取应用退出 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();
}

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

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

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

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

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

从输出结果可以看到我们的 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();

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

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

Process exits

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

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

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

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

有一个 SIGQUITConsoleLifetime 里有注册,石头哥他们库里没注册,给他们提了一个 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, );
#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, ));
PosixSignalRegistration.Create(PosixSignal.SIGQUIT, ctx => InvokeExitHandler(ctx, ));
PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => InvokeExitHandler(ctx, ));
#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 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);
}

输出结果如下:

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


展开阅读全文

页面更新:2024-05-23

标签:可能会   示例   初始化   进程   石头   参数   事件   代码   测试   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top