IHostedService 实现类的启动顺序由注册顺序决定
在
IServiceCollection中调用
AddHostedService<t>()</t>的先后顺序,直接决定了
IHostedService.StartAsync()的执行顺序。ASP.NET Core 主机会按注册顺序依次 await 所有
StartAsync()方法,**不会并发启动**。 若 A 依赖 B 已初始化(比如 B 初始化了共享资源),必须先注册 B,再注册 A 注册顺序不等于构造函数调用顺序 —— 构造注入仍遵循 DI 容器解析逻辑,但
StartAsync严格按
AddHostedService顺序串行执行 任意一个
StartAsync抛出未捕获异常,后续所有服务的
StartAsync都不会执行,主机启动失败
BackgroundService 的 StopAsync 调用顺序与 StartAsync 相反
BackgroundService是
IHostedService的抽象基类,它内部维护了一个
CancellationTokenSource,并在
StopAsync中触发取消信号。但关键点在于:**所有
IHostedService.StopAsync()按注册顺序的逆序执行**。 即:最后注册的服务最先被停止(LIFO) 这是为了支持“后启先停”的依赖关系:A 依赖 B 的服务,B 应该比 A 晚停,以确保 A 停止时 B 仍可用
BackgroundService自动处理了循环等待其后台任务结束(通过
ExecuteAsync返回的
Task),所以你的
StopAsync通常只需 await base.StopAsync(),无需手动 Cancel
StopAsync 超时会导致强制终止,且不保证执行完成
主机默认给所有
StopAsync总共 5 秒超时(可通过
IHostBuilder.UseShutdownTimeout()修改)。这个超时是全局的,不是每个服务单独计时。 如果多个
IHostedService的
StopAsync累计耗时超过该阈值,主机将直接调用
Environment.Exit(1)终止进程 即使某个服务的
StopAsync还在运行,也不会被等待 —— 没有“尽力而为”机制,超时即弃
BackgroundService的
base.StopAsync()会 await
ExecuteAsync返回的 Task,但如果该 Task 本身没响应取消(比如没检查
cancellationToken.IsCancellationRequested),就会拖垮整个关机流程
常见错误:在 BackgroundService.ExecuteAsync 中忽略 cancellation token
最典型的崩溃场景是:后台任务死循环且不响应取消,导致
StopAsync卡住,最终触发主机强制退出。
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// ❌ 错误:没有把 stoppingToken 传给异步操作,且循环内没检查
await DoWork(); // 如果 DoWork 内部不响应 token,这里可能永远不返回
}
catch (Exception ex)
{
_logger.LogError(ex, "Work failed");
}
await Task.Delay(1000, stoppingToken); // ✅ 正确:Delay 显式接受 token,能及时响应取消
}
}
所有阻塞或延迟操作(Task.Delay、
HttpClient.SendAsync、
Channel.Reader.ReadAsync等)必须传入
stoppingToken长时间 CPU 密集型工作需定期手动检查
stoppingToken.ThrowIfCancellationRequested()不要在
ExecuteAsync中 await 一个不接受 token 的第三方异步方法,除非你确认它内部可被中断 依赖管理和超时控制是实际部署中最容易出问题的地方,尤其是当多个
BackgroundService共享状态或资源时,启动/停止顺序 + 取消传播必须显式对齐,否则关机过程不可预测。
