为什么在 .NET Core 3.1 中停止 BackgroundService 时 IsCancellationRequested 未设置为 true?

Why is IsCancellationRequested not set to true on stopping a BackgroundService in .NET Core 3.1?

我已经阅读了我能找到的大多数关于 IHostApplicationLifetime 和 .NET Core 3.1 中的 CancellationToken 的文章,但我找不到这不起作用的原因。

我有一个简单的 BackgroundService,如下所示:

    public class AnotherWorker : BackgroundService
    {
        private readonly IHostApplicationLifetime _hostApplicationLifetime;

        public AnotherWorker(IHostApplicationLifetime hostApplicationLifetime)
        {
            _hostApplicationLifetime = hostApplicationLifetime;
        }

        public override Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine($"Process id: {Process.GetCurrentProcess().Id}");
            _hostApplicationLifetime.ApplicationStarted.Register(() => Console.WriteLine("Started"));
            _hostApplicationLifetime.ApplicationStopping.Register(() => Console.WriteLine("Stopping"));
            _hostApplicationLifetime.ApplicationStopped.Register(() => Console.WriteLine("Stopped"));

            return Task.CompletedTask;
        }

        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            Console.WriteLine("Executing");
            return Task.CompletedTask;
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
        // This actually prints "Stop. IsCancellationRequested: False". Why?
            Console.WriteLine($"Stop. IsCancellationRequested: {cancellationToken.IsCancellationRequested}");
            await base.StopAsync(cancellationToken);
        }
    }

默认添加ConsoleLifetime,监听Ctrl+C和SIGTERM,通知IHostApplicationLifetime。我猜 IHostApplicationLifetime 反过来应该取消所有 CancellationTokens? Here's a good article on the subject。那么为什么上面代码片段的输出如下?

Hosting starting
Started
Hosting started
(sends SIGTERM with `kill -s TERM <process_id>`)
Applicationis shuting down...
Stop. IsCancellationRequested: False
Stopped
Hosting stopped

我希望它记录 Stop. IsCancellationRequested: True

我希望能够将此令牌传递给其他服务调用,以便它们能够正常关闭。

传递给 StopAsyncCancellationToken 指示 BackgroundService 是否必须执行正常关机或硬关机。

您使用 kill -s TERM 停止进程,因此它发送 SIGTERM 信号要求应用程序正常关闭。因此 IsCancellationRequested 属性 仍然是假的。

要将令牌传递给其他服务调用,您必须提供自己的 CancellationToken。您可以使用 CancellationTokenSource 来管理令牌的创建和取消。

这里有很多不同的取消标记,以及几个不同的抽象(IHostApplicationLifetimeIHostedServiceBackgroundService)。理清一切都需要一段时间。您链接到的博客 post 很棒,但没有详细介绍 CancellationToken

首先,如果您要使用BackgroundService,我推荐reading the code。另外,我强烈建议不要覆盖 StartAsyncStopAsyncBackgroundService 以非常特殊的方式使用它们。

IHostedService has two methods. StartAsync starts the service running (possibly asynchronously); it takes a CancellationToken that indicates the "start" operation should be cancelled (I haven't checked, but I assume this token is only triggered if the app is shutdown almost immediately). Note that StartAsync needs to complete before the hosted service is considered in the "started" or "running" state. Similarly, StopAsync stops the service (possibly asynchronously). StopAsync is invoked when the application begins its graceful shutdown. There's a timeout for the graceful shutdown period, after which the application begins its "I'm serious now" shutdown. The CancellationToken for StopAsync表示从"graceful"过渡到"I'm serious now"。所以它 不是 在正常关机超时期间设置的 window。

如果你直接使用 BackgroundService 而不是 IHostedService(就像大多数人那样),你会得到 不同的 CancellationToken ExecuteAsync。这个 is set when BackgroundService.StopAsync is invoked - i.e., when the application has started its graceful shutdown. So it's roughly equivalent to IHostApplicationLifetime.ApplicationStopping,但仅限于单个托管服务。您可以预期 BackgroundWorker.ExecuteAsync CancellationToken 会在设置 IHostApplicationLifetime.ApplicationStopping 后不久设置。

请注意,所有这些 CancellationToken 代表不同的东西:

  • IHostedService.StartAsyncCancellationToken表示"abort the starting of this service".
  • IHostedService.StopAsyncCancellationToken表示"stop this service right now; you're out of the grace period".
  • IHostApplicationLifetime.ApplicationStopping 表示 "the graceful shutdown sequence for this entire application has started; everyone please stop what you are doing"。
    • 作为正常关机序列的一部分,将调用所有 IHostedService.StopAsync 方法。
  • BackgroundService.ExecuteAsyncCancellationToken表示"stop this service".

一个有趣的注意事项是 BackgroundService 类型通常看不到 "I'm serious now" 信号;他们只看到 "stop this service" 信号。这可能是因为 CancellationToken 表示的 "I'm serious now" 信号有点令人困惑。

如果您查看 the code for Host,关闭序列有 更多 个取消标记用于其关闭序列:

  1. IHost.StopAsync 需要 CancellationToken meaning "the stop should no longer be graceful".
  2. 然后starts a CancellationToken-based timeout for the graceful timeout period.
  3. ... 和 another linked CancellationToken 如果 或者 IHost.StopAsync 令牌被触发或者计时器结束则被触发。所以这个也意味着 "the stop should no longer be graceful".
  4. 下一个calls IHostApplicationLifetime.StopApplication, which cancels the IHostApplicationLifetime.ApplicationStopping CancellationToken.
  5. 然后 invokes StopAsync for each IHostedService,传递 "stop should no longer be graceful" 令牌。
    • 所有 BackgroundService 类型都有自己的 CancellationToken(在启动期间传递给 ExecuteAsync),这些取消标记是 cancelled by StopAsync.
  6. 最后,invokes IHostApplicationLifetime.NotifyStopped, which cancels the IHostApplicationLifetime.ApplicationStopped CancellationToken

我为 "no longer graceful" 信号计数 3(一个传入,一个计时器,一个连接这两个信号),在 IHostApplicationLifetime 上加上 2,每个 BackgroundService 加上 1,关闭期间总共使用了 5 + n 个取消令牌。 :)