当调用IHost.StopAsync时,为什么会两次调用IHostedService.StopAsync?

8

给定以下使用通用主机的.NET Core 2.2控制台应用程序:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace SimpleGenericHost
{
    class SimpleHostedService : IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Service started");
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Service stopped");
            return Task.CompletedTask;
        }
    }

    class Program
    {
        static async Task Main(string[] args)
        {
            var host = new HostBuilder()
                .ConfigureServices(services =>
                {
                    services.AddHostedService<SimpleHostedService>();
                })
                .Build();

            var runTask = host.RunAsync();
            await Task.Delay(5000);
            await host.StopAsync();
            await runTask;

        }
    }
}

当您运行它时,将输出以下内容:
Service started
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\projects\ConsoleApps\SimpleGenericHost\bin\Debug\netcoreapp2.2\
Service stopped
Service stopped

正如您所看到的,SimpleHostedService.StopAsync被调用了两次。为什么?

这是预期的吗?我有什么遗漏的吗?是否有其他方法可以停止主机,以便只调用一次IHostedService.StopAsync


1
StartAsync和StopAsync都是由框架自己调用的。您不必手动调用它们。 - Alberto
@Alberto。如果我需要以除了键入ctrl + c或发送SIGTERM之外的其他方式停止主机怎么办? - Jesús López
1个回答

12

这是因为它被调用了两次 - 一次是在延迟之后,另一次是当服务实际停止时。 它不应该在使用RunAsync时被调用。

要在超时后停止,请使用带有超时的CancellationTokenSource,并将其令牌传递给RunAsync

var timeoutCts=new CancellationTokenSource(5000);

await host.RunAsync(timeoutCts.Token);
说明 StopAsync 不会停止服务,它用于通知服务需要停止。当应用程序停止时,托管服务基础结构会自行调用该方法。
.NET Core 是开源的,这意味着您可以查看 RunAsync 源代码,请参见此链接。RunAsync 启动主机然后等待终止:
await host.StartAsync(token);

await host.WaitForShutdownAsync(token);

WaitForShutdownAsync 方法监听终止请求,这可以来自控制台或通过对 IHostApplicationLifetime.StopApplication 的显式调用。一旦发生这种情况,它会自行调用 StopAsync

await waitForStop.Task;

// Host will use its default ShutdownTimeout if none is specified.
await host.StopAsync();

如果您想自己管理应用程序的生命周期,那么就应该使用StartAsync而不是RunAsync

但在这种情况下,只有在超时时才需要停止应用程序。您可以通过将取消令牌传递给RunAsync来轻松实现这一点,该令牌仅在超时后触发,可以通过CancellationTokenSource(int)构造函数进行设置:

var timeoutCts=new CancellationTokenSource(5000);

await host.RunAsync(timeoutCts.Token);

1
@onefootswill 不是的,Run或者RunAsync实际上会启动应用程序和所有服务。例如,在ASP.NET Core应用程序中,直到调用Run之前,没有任何东西开始运行。这不是一种方法的问题 - 你认为Run在底层调用了什么?host.Start.Async()。在每个情况下,你如何通知应用程序停止?你调用StopApplication。事实上,我已经链接到了显示相同调用的源代码,就像链接的答案中所示。 - Panagiotis Kanavos
@PanagiotisKanavos 好的。我刚刚观察到,除非我使用 Ctrl-C 杀掉应用程序(如果该代码在运行调用之后),否则我的所有代码都不会运行。这就是为什么我认为 Run 是阻塞的原因。这就是让我开始进行这项调查的原因。我会调用 host.Run(),但什么也不会发生(直到按下 Ctrl-C)。 - onefootswill
1
实际上,在控制台应用程序中根本不需要调用RunStartApplication。只需使用主机来检索服务和配置,完成后简单地退出主函数即可。 - Panagiotis Kanavos
1
如果那段代码出现在Run被调用之后,那么它会在应用程序终止时被执行。因为Run是运行应用程序的方法,当应用程序终止时,它也随之退出。任何在Run被调用之后的代码都将在终止期间被执行。 - Panagiotis Kanavos
1
@onefootswill 当你需要托管和运行应用程序时,你需要使用 Run(Async)。WinForms 应用程序不需要另一个主机,它已经自己托管。所有其他服务,例如 DI、配置、日志记录,都可以独立使用。通用主机是访问它们所有的便捷地方。 - Panagiotis Kanavos
显示剩余7条评论

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