ASP.NET Core 8 Web API与BackgroundService无法同时启动问题求助
我有一个本地正常运行的ASP.NET Core 8 Web API项目,Swagger UI可通过localhost地址正常打开。需求是定期更新数据库中某张表的数据,并将该数据提供给API接口使用。
我使用BackgroundService实现了后台定时任务,代码如下:
using Cronos; public class MyUpdateJobService : BackgroundService { private readonly CronExpression _cronExpression; private DateTimeOffset _nextRun = DateTimeOffset.MinValue; private readonly IDbService _dbService; public MyUpdateJobService(IConfiguration configuration, IDbService dbService) { _dbService = dbService; _cronExpression = CronExpression.Parse(configuration["CronExpression"]);//从appsettings读取 } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while(!stoppingToken.IsCancellationRequested) { var now = DateTimeOffset.Now; if(now >= _nextRun) { InvokeDbOperation(); _nextRun = _cronExpression.GetNextOccurrence(now, TimeZoneInfo.Local) ?? DateTimeOffset.MaxValue; } } await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } private void InvokeDbOperation() { //准备数据 - 调用Scoped服务获取数据 //更新数据库数据操作 //整个过程耗时约10分钟 } }
Program.cs配置:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddAllServices(); // 注册DI服务 builder.Services.AddHostedService<MyUpdateJobService>(); builder.Services.Configure<HostOptions>(option => { option.ServicesStartConcurrently = true; option.ServicesStopConcurrently = true; }); ... var app = builder.Build(); ... app.Run();
AppSettings配置:
"CronExpression": "*/30 * * * *"//每30分钟执行一次
添加上述配置后,启动Web API时,后台服务会先执行首次任务(耗时约10分钟),期间Web服务器无法启动,localhost地址无法访问;只有等待后台任务完成后,Web服务器才会启动,后续两者能并发运行。
我需要实现Web服务器与后台服务在首次启动时即可并发运行,请问需要修改哪些配置或代码?
问题的核心是后台服务的同步阻塞代码导致宿主启动流程被卡住,结合代码具体修改如下:
1. 将同步任务改为异步执行
InvokeDbOperation是同步方法,且耗时10分钟,而ExecuteAsync在遇到第一个await之前会同步执行代码,这会直接阻塞宿主的启动线程,导致Web服务器无法启动。
修改步骤:
- 将
InvokeDbOperation改为异步方法 - 在
ExecuteAsync中使用await调用该方法,释放线程让宿主继续启动Web服务
修改后的代码:
// 改写为异步方法 private async Task InvokeDbOperationAsync() { using var scope = _scopeFactory.CreateScope(); var dbService = scope.ServiceProvider.GetRequiredService<IDbService>(); // 异步执行数据准备和更新操作 // 示例:await dbService.FetchDataAsync(); // await dbService.UpdateTableAsync(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var now = DateTimeOffset.Now; if (now >= _nextRun) { // 异步调用,不阻塞启动线程 await InvokeDbOperationAsync(); _nextRun = _cronExpression.GetNextOccurrence(now, TimeZoneInfo.Local) ?? DateTimeOffset.MaxValue; } // 将Task.Delay移到循环内部,确保每次循环后等待30秒 await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } }
2. 修正Scoped服务的调用方式
因为BackgroundService是单例生命周期,直接注入Scoped的IDbService会导致生命周期不匹配,需要通过IServiceScopeFactory创建临时作用域来获取Scoped服务:
修改构造函数注入:
private readonly CronExpression _cronExpression; private DateTimeOffset _nextRun = DateTimeOffset.MinValue; private readonly IServiceScopeFactory _scopeFactory; public MyUpdateJobService(IConfiguration configuration, IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; _cronExpression = CronExpression.Parse(configuration["CronExpression"]); }
3. 调整循环内的延迟逻辑
原代码中await Task.Delay在while循环外部,导致首次进入循环后会无限轮询,不会释放线程。将延迟移到循环内部后,每次循环都会等待30秒,避免不必要的资源消耗。
补充说明
你配置的HostOptions(ServicesStartConcurrently = true)是正确的,它允许多个后台服务并发启动,但问题出在单个后台服务内部的同步阻塞代码,只要将同步操作改为异步,就能让宿主在后台任务执行的同时启动Web服务器。
内容的提问来源于stack exchange,提问作者cgsabari

