调用IHost.StopAsync时IHostedService.StopAsync被调用两次的原因及解决
先看你给出的代码和运行结果:
示例代码
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被调用两次的问题,下面逐一解答:
1. 重复调用的原因
这是.NET Core 2.2通用主机的设计缺陷:
- 调用
host.RunAsync()时,方法不仅会启动主机,还会内部注册终止信号(如Ctrl+C)的处理逻辑,同时等待主机停止令牌触发。 - 手动调用
host.StopAsync()会主动触发主机停止流程,调用所有IHostedService的StopAsync。 - 但
RunAsync内部检测到主机状态变为停止后,会再次执行一次停止清理逻辑,导致StopAsync被第二次调用。
简单来说就是手动停止和RunAsync的内部停止流程没有做好协调,导致了重复触发。
2. 是否符合预期?有没有配置遗漏?
在.NET Core 2.2版本中,这种行为符合当时框架的实现逻辑,但绝对不是开发者想要的预期结果。这不算配置遗漏,纯粹是早期通用主机的设计问题——微软在后续版本中已经修复了这个bug。
3. 如何让StopAsync仅调用一次?
有几种可行的解决方案:
方案一:用取消令牌替代手动调用StopAsync
通过CancellationTokenSource控制主机停止,让RunAsync统一处理整个停止流程:
static async Task Main(string[] args) { using var cts = new CancellationTokenSource(); var host = new HostBuilder() .ConfigureServices(services => { services.AddHostedService<SimpleHostedService>(); }) .Build(); var runTask = host.RunAsync(cts.Token); await Task.Delay(5000); cts.Cancel(); // 触发取消信号,由RunAsync内部处理停止 await runTask; }
这种方式下,停止逻辑由RunAsync主导,只会调用一次StopAsync。
方案二:升级到.NET Core 3.0及以上版本
微软在.NET Core 3.0时彻底重构了通用主机实现,修复了这个重复调用问题。如果项目允许升级框架版本,这是最一劳永逸的办法——升级后即使手动调用host.StopAsync(),也只会触发一次停止逻辑。
方案三:手动控制主机生命周期,不用RunAsync
绕开RunAsync的内部逻辑,手动启动和停止主机:
static async Task Main(string[] args) { var host = new HostBuilder() .ConfigureServices(services => { services.AddHostedService<SimpleHostedService>(); }) .Build(); await host.StartAsync(); // 手动启动主机 Console.WriteLine("Application started. Press Ctrl+C to shut down."); await Task.Delay(5000); await host.StopAsync(); // 手动停止,仅触发一次StopAsync await host.WaitForShutdownAsync(); // 可选,等待主机完全关闭 }
这种方式完全由你掌控主机的启动和停止,不会出现重复调用问题。
内容的提问来源于stack exchange,提问作者Jesús López




