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

10

我已经阅读了大部分关于IHostApplicationLifetime和CancellationToken在.NET Core 3.1中的文章,但我找不到为什么这不起作用的原因。

我有一个简单的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?这是一篇关于此主题的好文章。那么为什么上面代码片段的输出结果会是以下内容?
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”。
我希望能够将此令牌传递给其他服务调用,以使它们具有优雅关闭的能力。

https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.hostinghostbuilderextensions.runconsoleasync?view=dotnet-plat-ext-3.1 - Hans Passant
我认为IsCancellationRequested与相同令牌中的不同。为了优雅地关闭,您需要在ExecuteAsync上执行该操作。当您在那里有一个长时间运行的任务并检查IsCancellationRequested时,它将为真。 CloseAsync只是用于清理一些数据,而不是用于优雅地关闭。传递到ExecuteAsync的令牌与CloseAsync不同。 - CodeNotFound
2个回答

35
这里有很多不同的取消令牌和几种不同的抽象(IHostApplicationLifetimeIHostedServiceBackgroundService)。解开它们之间的关系需要一些时间。你链接的博客文章非常好,但并没有详细介绍 CancellationToken
首先,如果你要使用 BackgroundService,我建议查看代码。此外,我强烈建议不要覆盖 StartAsyncStopAsyncBackgroundService 以非常特定的方式使用这些方法。 IHostedService 有两个方法。 StartAsync 启动服务运行(可能是异步的);它接受一个 CancellationToken,指示“启动”操作应该被取消。注意,StartAsync 需要在托管服务被认为处于“已启动”或“正在运行”状态之前完成。同样地,StopAsync 停止服务(可能是异步的)。当应用程序开始优雅关闭时,会调用 StopAsync。优雅关闭有一个超时期限,超过此期限后,应用程序开始“我现在很认真了”关闭。StopAsync 的 CancellationToken 表示从“优雅”到“我现在很认真了”的转换。因此,在优雅关闭超时窗口期间,不会设置该令牌。
如果您直接使用BackgroundService而不是IHostedService(像大多数人一样),则在ExecuteAsync中会得到一个不同的CancellationToken。当调用BackgroundService.StopAsync时,这个CancellationToken被设置-即应用程序已经启动了优雅的关闭。因此,它大致相当于IHostApplicationLifetime.ApplicationStopping,但仅限于单个托管服务。您可以期望BackgroundWorker.ExecuteAsyncCancellationTokenIHostApplicationLifetime.ApplicationStopping之后不久被设置。
请注意,所有这些CancellationToken都代表不同的东西:
  • IHostedService.StartAsyncCancellationToken表示“中止此服务的启动”。
  • IHostedService.StopAsyncCancellationToken表示“立即停止此服务;您超出了宽限期”。
  • IHostApplicationLifetime.ApplicationStopping表示“此整个应用程序的优雅关闭序列已启动,请停止您正在做的一切”。
    • 作为优雅关闭序列的一部分,将调用所有IHostedService.StopAsync方法。
  • BackgroundService.ExecuteAsyncCancellationToken表示“停止此服务”。
有趣的是,BackgroundService类型通常不会看到“我现在很认真”的信号;它们只会看到“停止此服务”的信号。这可能是因为由CancellationToken表示的“我现在很认真”的信号有点令人困惑。
如果您查看Host的代码,则关闭序列在其关闭序列中使用了更多的取消令牌。
  1. IHost.StopAsync接受一个CancellationToken表示"停止不再优雅"
  2. 然后它启动基于CancellationToken的超时以进行优雅的超时周期
  3. ...和另一个相关的CancellationToken,如果要么IHost.StopAsync标记被触发,要么计时器过期,就会触发。所以这个也表示"停止不再优雅"。
  4. 接下来,它调用IHostApplicationLifetime.StopApplication,这将取消IHostApplicationLifetime.ApplicationStopping CancellationToken
  5. 然后,对于每个IHostedService,它调用StopAsync,并传递"停止不再优雅"的令牌。
    • 所有BackgroundService类型都有自己的CancellationToken (在启动期间传递给ExecuteAsync),这些取消令牌是StopAsync取消的
  6. 最后,它调用IHostApplicationLifetime.NotifyStopped,并取消IHostApplicationLifetime.ApplicationStopped CancellationToken

我统计了“no longer graceful”信号的数量为3(一个传递,一个定时器,一个将这两者连接起来),加上IHostApplicationLifetime中的2个,再加上每个BackgroundService的1个,总共在关闭期间使用了5 + n个取消令牌。:)


1
现在当你说出来时,一切都更合理了。确实这里有很多事情要处理。不幸的是,到处使用相同的“CancellationToken”类型使得代码有点不太自解释。你必须将它们视为非常上下文特定的标记,然后就变得更容易推理了。谢谢Stephen! - Jim Aho
如果我有一个客户端需要关闭连接,比如服务总线订阅客户端。这个API是异步的,我想要等待它完成,但是我不确定在哪里调用await _subscriptionClient.CloseAsync(而不覆盖StopAsync方法)。据我所知,IHostApplicationLifetime不支持异步操作,而且我也不能使用传递给ExecuteAsyncCancellationToken(因为token.Register也不是异步的)。你有什么想法吗?谢谢。 - Jim Aho
如果您正在使用 BackgroundService,我建议在代码退出 ExecuteAsync 之前的 finally 分支中调用 CloseAsync。额外加分:使用 IAsyncDisposable。 :) - Stephen Cleary
问题在于我在ExecuteAsync中的代码本身不是异步的,而只是一个同步调用,它只是实例化了一个订阅客户端并连接了一个消息处理程序。此外,我认为我不能在这里使用IAsyncDisposable,因为我想要“优雅地”调用CloseAsync来停止订阅客户端接收更多的消息,而这比DisposeAsync被调用的时间更早。更新:我想我的真正问题在于订阅客户端似乎不支持取消令牌,所以我无法将令牌从ExecuteAsync传递到下一层。 - Jim Aho
1
好的,听起来像是客户端API问题。如果从执行同步“Receive”的线程不同的线程调用CloseAsync是合适的,那么您可以将其注册到传递给ExecuteAsyncCancellationToken中(使用async void lambda或sync-over-async hack)。这不会很漂亮,但由于客户端不支持取消并且需要异步拆除(奇怪的组合),因此您最终不会得到一个漂亮的解决方案。 - Stephen Cleary

0

CancellationToken传递给StopAsync,指示BackgroundService必须执行优雅的关闭或硬关闭。

您可以使用kill -s TERM停止进程,因此它发送SIGTERM信号请求应用程序优雅地关闭。因此,IsCancellationRequested属性仍为false。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接