为何Task.Delay()会触发IApplicationLifetime的取消事件?
首先,你的问题核心是误解了IApplicationLifetime.ApplicationStarted令牌的设计意图。
ApplicationStarted这个取消令牌的作用是:当应用完全启动完成后,它会被触发取消,用来通知那些等待启动完成的代码“启动已经完成了”——它并不是用来判断应用是否应该继续运行的令牌。所以当你的应用启动完成(也就是控制台输出Application started. Press Ctrl+C to shut down.之后),ApplicationStarted.IsCancellationRequested就会变为true,这直接导致你的循环条件不满足,托管服务就退出了。
而你期望的是服务仅在调用StopApplication()时停止,所以你的循环条件里不应该包含对ApplicationStarted.IsCancellationRequested的判断。
修正后的托管服务代码示例
我们只需要保留stopToken和ApplicationStopping的判断(其实stopToken本身已经包含了应用停止的信号,因为BackgroundService的ExecuteAsync传入的stopToken会在应用停止时被触发),同时注意不要在服务里主动调用_host.StopAsync()——因为当应用收到停止信号时,Host会自动停止所有托管服务,你不需要手动触发:
public class BrowserWorkerHostedService : BackgroundService { private readonly IHostApplicationLifetime _lifetime; // 注意:.NET Core 3.0+ 推荐使用 IHostApplicationLifetime 替代过时的 IApplicationLifetime public BrowserWorkerHostedService(IHostApplicationLifetime lifetime) { _lifetime = lifetime; } protected override async Task ExecuteAsync(CancellationToken stopToken) { // 只需要监听停止相关的令牌,去掉对 ApplicationStarted 的判断 while (!_lifetime.ApplicationStopping.IsCancellationRequested && !stopToken.IsCancellationRequested) { Console.WriteLine($"{nameof(BrowserWorkerHostedService)} is working. {DateTime.Now.ToString()}"); await Task.Delay(1_000, stopToken); } Console.WriteLine($"End {nameof(BrowserWorkerHostedService)}"); } } public class EmailWorkerHostedService : BackgroundService { private readonly IHostApplicationLifetime _lifetime; public EmailWorkerHostedService(IHostApplicationLifetime lifetime) { _lifetime = lifetime; } protected override async Task ExecuteAsync(CancellationToken stopToken) { while (!_lifetime.ApplicationStopping.IsCancellationRequested && !stopToken.IsCancellationRequested) { Console.WriteLine($"{nameof(EmailWorkerHostedService)} is working. {DateTime.Now.ToString()}"); await Task.Delay(1_000, stopToken); } Console.WriteLine($"End {nameof(EmailWorkerHostedService)}"); } }
额外说明
- 关于
IApplicationLifetime的过时问题:在.NET Core 3.0及以后的版本中,IApplicationLifetime已经被标记为过时,官方推荐使用IHostApplicationLifetime,两者的API基本一致,但后者是更现代的替代方案。 stopToken的简化使用:BackgroundService传入的stopToken会自动关联到应用的停止信号,所以你甚至可以只判断!stopToken.IsCancellationRequested,代码会更简洁:while (!stopToken.IsCancellationRequested) { Console.WriteLine($"{nameof(BrowserWorkerHostedService)} is working. {DateTime.Now.ToString()}"); await Task.Delay(1_000, stopToken); }- 避免手动停止Host:你的原代码里在服务退出时调用
_host.StopAsync(),这会导致连锁停止,其实当应用收到停止信号(比如Ctrl+C或者调用StopApplication()),Host会自动停止所有托管服务,不需要手动触发。
这样修改后,你的托管服务就会一直运行,直到你调用_lifetime.StopApplication()或者按下Ctrl+C才会停止,符合你的预期。
内容的提问来源于stack exchange,提问作者Alex

