定时任务之BackgroundService的详细教程
一、BackgroundService是什么
在 .NET 中,BackgroundService 是实现后台定时任务的标准方式。它继承自 IHostedService,能够随应用程序启动而自动运行,并在应用关闭时优雅停止。
官方解释:
BackgroundService 是 .NET 提供的一个抽象基类,位于 Microsoft.Extensions.Hosting 命名空间,专门用来简化长时间运行的后台任务的实现。
它实现了 IHostedService 接口,并封装了 StartAsync 和 StopAsync 的常规处理,你只需要重写一个方法:
ExecuteAsync(CancellationToken stoppingToken):在这里编写你的后台逻辑,通常是一个循环,直到 stoppingToken 发出取消信号时退出。
二、 怎么实现定时任务
- 核心实现步骤
第一步:创建 定时任务服务类
继承 BackgroundService 并重写 ExecuteAsync 方法。这是任务逻辑的核心入口。
启动后会自动执行ExecuteAsync 方法
csharp
using ModbusDome.IService;
namespace ModbusDome.Services
{
public class MyBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<MyBackgroundService> _logger;
public MyBackgroundService(ILogger<MyBackgroundService> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
// protected override async Task ExecuteAsync(CancellationToken stoppingToken)
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("开始后台服务");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//2、开始执行任务
// 关键点:在后台服务中,直接解析 Scoped 服务会导致问题,因为后台服务的生命周期是 Singleton,而 Scoped 服务的生命周期是每个请求或作用域。
// 关键点:手动创建一个生命周期作用域
using (var scope = _serviceScopeFactory.CreateScope())
{
//HalloService: IHalloService
// 从新创建的作用域中解析 Scoped 服务
var scopedService = scope.ServiceProvider.GetRequiredService<IHalloService>();
scopedService.SayHallo();
}
// 3. 等待到那个时间点
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (TaskCanceledException)
{
_logger.LogWarning("服务在等待期间被取消。");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "服务在等待期间发生了未知错误。");
break;
}
}
_logger.LogInformation("结束后台服务");
}
}
}
2、Program注册服务
csharp
//在 Program.cs 中将服务注册为托管服务。
var builder = WebApplication.CreateBuilder(args);
// 其他服务注册...
builder.Services.AddScoped<IMyScopedService, MyScopedService>();
// 注册后台服务
builder.Services.AddHostedService<MyTimedHostedService>();
var app = builder.Build();
app.Run();
3、代码解释
3.1、CancellationToken结构体的作用
1、负责发起取消请求。
持有者可以通过调用 cts.Cancel() 或 cts.CancelAfter(TimeSpan)来触发取消信号。
2、CancellationToken:"信号接收器"
这是一个结构体(Value Type),轻量且可随意复制传递。
本程序中的SayHallo方法中其实只有两句代码
Console.WriteLine(DateTime.Now.ToString());
Console.WriteLine("Hallo!");
4、运行结果

三、程序完善
1、添加一个 bool _isExecuting = false;标志位用于防止上次一个任务没执行完,又开始一个新的任务。
2、将业务逻辑单独提取到一个方法中DoWorkAsync();
3、将定时器由Task.Delay更换为PeriodicTimer
csharp
using ModbusDome.IService;
using System.Timers;
namespace ModbusDome.Services
{
public class MyBackgroundService : BackgroundService
{
// 1. 通过一个标志位来控制任务的执行,确保上一个任务完成前不启动新的任务
private volatile bool _isExecuting = false;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<MyBackgroundService> _logger;
private readonly PeriodicTimer _timer = new(TimeSpan.FromSeconds(30));
public MyBackgroundService(ILogger<MyBackgroundService> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
// protected override async Task ExecuteAsync(CancellationToken stoppingToken)
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("开始后台服务");
try
{
// 每 5 分钟执行一次TimeSpan.FromMinutes(5)
//每 30 秒执行一次
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
// WaitForNextTickAsync 会等待下一个节拍,如果 stoppingToken 被触发则返回 false
// while (await timer.WaitForNextTickAsync(stoppingToken))
// while (!stoppingToken.IsCancellationRequested)
// WaitForNextTickAsync 会等待下一个节拍,如果 stoppingToken 被触发则返回 false
while (await timer.WaitForNextTickAsync(stoppingToken))
{
if (!_isExecuting)
{
// 1. 设置标志位,表示任务正在执行
_isExecuting = true;
try
{
//2、开始执行任务
await DoWorkAsync();
// 3. 等待到那个时间点
//await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (TaskCanceledException)
{
_logger.LogWarning("服务在等待期间被取消。");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "服务在等待期间发生了未知错误。");
break;
}
finally
{
_isExecuting = false; // 关键:确保无论如何都重置标志
}
}
else
{
_logger.LogWarning("任务仍在运行中,跳过本次触发");
// 4. 等待1秒后再次检查,避免过于频繁地检查
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}
}
catch (OperationCanceledException)
{
// 当 stoppingToken 被取消时,WaitForNextTickAsync 会抛出此异常
// 这里表示服务正在正常停止,可以记录日志
_logger.LogInformation("服务正在正常停止");
}
_logger.LogInformation("结束后台服务");
}
/// <summary>
/// 业务处理逻辑
/// </summary>
/// <returns></returns>
private async Task DoWorkAsync()
{
// 业务逻辑
// 关键点:在后台服务中,直接解析 Scoped 服务会导致问题,因为后台服务的生命周期是 Singleton,而 Scoped 服务的生命周期是每个请求或作用域。
// 关键点:手动创建一个生命周期作用域
using (var scope = _serviceScopeFactory.CreateScope())
{
//HalloService: IHalloService
// 从新创建的作用域中解析 Scoped 服务
var scopedService = scope.ServiceProvider.GetRequiredService<IHalloService>();
scopedService.SayHallo();
}
}
/// <summary>
/// 显示地释放 PeriodicTimer 资源,确保在服务停止时正确清理资源。
/// </summary>
public override void Dispose()
{
_timer.Dispose();
base.Dispose();
}
}
}
- 常见陷阱与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 服务启动但不执行 | 未重写 ExecuteAsync 或注册顺序错误 | 确保重写 ExecuteAsync 且包含异步等待逻辑;在 Program.cs 中显式调用 AddHostedService()。 |
| 内存泄漏/资源未释放 | 未正确使用 using 或 Dispose | 使用 using var timer 和 using var scope 确保资源自动释放。 |
| 多实例重复执行 | 多个服务器实例同时运行同一任务 | 单机 Timer 无法解决分布式问题。需引入分布式锁(如 Redis Lock、数据库应用锁 sp_getapplock)确保同一时刻只有一个实例执行。 |
| 任务堆积/雪崩 | 任务执行时间超过定时间隔 | PeriodicTimer 会等待上一轮完成。若任务耗时过长,需优化性能或调整间隔。对于关键任务,建议设置超时控制(如 WaitAsync)。 |