You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 05:38:21