0Pricing
C# Academy · 课时

IHostedService 与 BackgroundService

实现 IHostedService 和抽象的 BackgroundService,在 .NET 主机中运行后台工作。

IHostedService 与 BackgroundService 是 CoddyKit 上的免费 C# Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

什么是后台服务

后台服务会与您的 ASP.NET Core 应用并行执行长期运行的工作——处理队列、运行计划任务或监控资源。.NET 提供两个接口:IHostedService(最简形式)和抽象的 BackgroundService(适合编写循环)。

IHostedService 接口

IHostedService 有两个方法:StartAsync(主机启动时调用)和 StopAsync(平稳关闭期间调用)。它非常适合执行一次性的启动或关闭任务。

public class DatabaseMigratorService : IHostedService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public DatabaseMigratorService(IServiceScopeFactory sf) => _scopeFactory = sf;

    public async Task StartAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await db.Database.MigrateAsync(ct);
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

builder.Services.AddHostedService<DatabaseMigratorService>();

BackgroundService:长时间运行的循环

BackgroundService 是一个实现了 IHostedService 的抽象类,并提供 ExecuteAsync(CancellationToken)——该方法会在应用的整个生命周期内运行。

public class HeartbeatService : BackgroundService
{
    private readonly ILogger<HeartbeatService> _logger;

    public HeartbeatService(ILogger<HeartbeatService> l) => _logger = l;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        _logger.LogInformation("Heartbeat service started");

        while (!ct.IsCancellationRequested)
        {
            _logger.LogInformation("Heartbeat: {Time}", DateTime.UtcNow);
            await Task.Delay(TimeSpan.FromSeconds(30), ct);
        }

        _logger.LogInformation("Heartbeat service stopping");
    }
}

builder.Services.AddHostedService<HeartbeatService>();

使用作用域服务

后台服务是单例。要使用作用域服务(例如 DbContext),请始终使用 IServiceScopeFactory 为每个工作单元创建新的作用域。

public class DataSyncService : BackgroundService
{
    private readonly IServiceScopeFactory _factory;
    private readonly ILogger<DataSyncService> _log;

    public DataSyncService(IServiceScopeFactory f, ILogger<DataSyncService> l)
    { _factory = f; _log = l; }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using (var scope = _factory.CreateScope())
            {
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                await SyncDataAsync(db, ct);
            } // scope and DbContext disposed here

            await Task.Delay(TimeSpan.FromMinutes(5), ct);
        }
    }
}

平稳关闭

主机关闭时会触发取消令牌。您的 ExecuteAsync 应当检查该令牌并执行清理。主机默认最多等待 5 秒,然后才会强制停止。

protected override async Task ExecuteAsync(CancellationToken ct)
{
    try
    {
        while (!ct.IsCancellationRequested)
        {
            await DoWorkAsync(ct);
            await Task.Delay(1000, ct);
        }
    }
    catch (OperationCanceledException)
    {
        // Normal shutdown — not an error
        _logger.LogInformation("Service stopping due to cancellation");
    }
    finally
    {
        // Cleanup
        await CleanupAsync();
    }
}

基于通道的后台队列

一种常见模式是:后台服务从通道队列中读取工作项,而 HTTP 端点负责将工作项加入队列。这样可以将请求处理与工作处理解耦。

public class BackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue
        = Channel.CreateBounded<Func<CancellationToken, Task>>(100);

    public ValueTask QueueAsync(Func<CancellationToken, Task> job)
        => _queue.Writer.WriteAsync(job);

    public IAsyncEnumerable<Func<CancellationToken, Task>> DequeueAllAsync(
        CancellationToken ct)
        => _queue.Reader.ReadAllAsync(ct);
}

// Register and use:
builder.Services.AddSingleton<BackgroundTaskQueue>();
builder.Services.AddHostedService<QueueProcessorService>();

后台服务中的错误处理

ExecuteAsync 中未处理的异常可能会悄然停止服务。请在主循环中使用 try/catch 进行包装并记录错误,以便服务即使遇到单个故障也能继续运行。

protected override async Task ExecuteAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        try
        {
            await ProcessNextBatchAsync(ct);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            break; // normal shutdown
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Background processing error. Retrying in 5s.");
            await Task.Delay(5000, ct); // backoff before retry
        }
    }
}

StopAsync 与关闭超时

重写 StopAsync 来执行清理。主机会传入关闭令牌——如果清理时间超过超时时间,进程仍会被终止。

public override async Task StopAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Service is stopping...");

    // Signal internal work to stop
    _internalCts.Cancel();

    // Wait for the Execute loop to complete (up to timeout)
    await base.StopAsync(stoppingToken);

    _logger.LogInformation("Service stopped");
}

// Increase the shutdown timeout if needed:
builder.Services.Configure<HostOptions>(opt =>
    opt.ShutdownTimeout = TimeSpan.FromSeconds(30));

多个后台服务

可以注册多个托管服务——它们都会并发运行。主机会按注册顺序启动它们,并按相反顺序停止它们。

builder.Services.AddHostedService<DatabaseMigratorService>(); // startup task
builder.Services.AddHostedService<MetricsCollectorService>();  // continuous
builder.Services.AddHostedService<EmailNotificationService>(); // continuous
builder.Services.AddHostedService<CacheWarmupService>();       // startup task

// All run concurrently after app start
// Stopped in reverse registration order on shutdown

实际案例:订单处理工作程序

一个从数据库队列中处理待处理订单的后台服务,并正确处理作用域和错误。

public class OrderProcessorService : BackgroundService
{
    private readonly IServiceScopeFactory _factory;
    private readonly ILogger<OrderProcessorService> _log;

    public OrderProcessorService(IServiceScopeFactory f, ILogger<OrderProcessorService> l)
    { _factory = f; _log = l; }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                using var scope = _factory.CreateScope();
                var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
                int count = await processor.ProcessPendingAsync(ct);
                _log.LogInformation("Processed {Count} orders", count);
            }
            catch (Exception ex) when (!ct.IsCancellationRequested)
            {
                _log.LogError(ex, "Order processing failed");
            }
            await Task.Delay(TimeSpan.FromSeconds(10), ct);
        }
    }
}

快速检查

为什么后台服务必须使用 IServiceScopeFactory,而不能直接注入 DbContext?

回顾:IHostedService 与 BackgroundService

关键要点:

  • IHostedService:StartAsync/StopAsync — 用于启动或关闭任务
  • BackgroundService:ExecuteAsync 循环 — 用于持续运行的后台工作
  • 在后台服务中使用作用域依赖项时,始终使用 IServiceScopeFactory
  • 在每次异步等待时检查取消令牌,以实现平稳关闭
  • 使用 try/catch 包装循环主体,防止服务因错误而终止
  • 如果清理需要超过 5 秒,请使用 HostOptions.ShutdownTimeout

常见问题解答

「IHostedService 与 BackgroundService」课时是免费的吗?

是的 — 「IHostedService 与 BackgroundService」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「IHostedService 与 BackgroundService」这节课中我会学到什么?

实现 IHostedService 和抽象的 BackgroundService,在 .NET 主机中运行后台工作。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「IHostedService 与 BackgroundService」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. IHostedService 与 BackgroundService
  2. Worker Service 项目
  3. 周期性任务与计时器
  4. Quartz.NET 计划任务
← 返回 C# Academy