跳转到主内容
极星编程网:以代码为星,赴技术山海!

C#怎么创建后台定时任务_C# IHostedService与BackgroundService【核心】

直接用 Timer 易致应用关闭时任务未停、Scoped 服务解析失败;应改用 BackgroundService,在 ExecuteAsync 中每次循环创建新 ServiceScope 来安全使用 DbContext。 为什么直接用 Timer 容易出问题 在 ASP.NET Core 里硬写
System.Threading.Timer
启动后台任务,最常踩的坑是:应用关闭时任务还在跑,导致数据写一半、连接没释放、日志错乱。更隐蔽的是,
Timer
回调不绑定
IServiceScope
,你没法安全解析
IDbContext
或其他 Scoped 服务——一用就报
Cannot resolve scoped service from root provider
。 根本原因:Timer 没接入 ASP.NET Core 的生命周期管理,它不知道应用正在停止,也不懂依赖注入容器的边界。 别在
Startup.Configure
或
Program.Main
里 new Timer 别把 DbContext 直接塞进 Timer 回调闭包里(哪怕加了
using
) 需要定时执行数据库操作?必须走 Scoped Service + 正确的 Scope 创建流程 IHostedService 和 BackgroundService 到底选哪个
IHostedService
是接口,只定义了
StartAsync
和
StopAsync
;
BackgroundService
是微软提供的抽象基类,内部已帮你处理了取消令牌传递、异常捕获和“运行中”状态维护。95% 的场景直接继承
BackgroundService
更稳。 如果你需要极简控制(比如只启不关、或自定义启动失败重试逻辑),才考虑手动实现
IHostedService
。但要注意:
StopAsync
必须 await 所有正在跑的任务,否则 Kestrel 会等超时后强行杀进程。 用
BackgroundService
:重写
ExecuteAsync(CancellationToken stoppingToken)
,里面写 while 循环 +
await Task.Delay(..., stoppingToken)
别在
ExecuteAsync
里 throw 未捕获异常——它会让宿主认为服务崩溃,可能触发重启
stoppingToken
不只是用来 Delay,所有可取消的 IO(如
HttpClient.GetAsync
、
context.SaveChangesAsync
)都得传进去 怎么在 BackgroundService 里安全用 DbContext DbContext 默认注册为 Scoped,而
BackgroundService
是 Singleton。直接注入会报错。正确做法是在每次循环里创建新 Scope: C知道 CSDN推出的一款AI技术问答工具 下载
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var scope = _serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); await context.Logs.AddAsync(new Log { Message = "tick" }, stoppingToken); await context.SaveChangesAsync(stoppingToken); await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } }
关键点: 必须用
IServiceProvider
(不是
IServiceScope
)来 CreateScope ——
_serviceProvider
要在构造函数里接收并保存 每个循环都要 new scope,不能复用;scope 生命周期必须严格匹配单次任务 如果任务耗时长(比如导出报表),建议加超时控制:
using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); cts.CancelAfter(TimeSpan.FromMinutes(10));
本地调试时任务不触发?检查这三处 最常见的假“不执行”其实是日志没刷出来,或任务间隔太长没等到下一次触发。先确认基础链路通不通: 确保服务已注册:
services.AddHostedService()
,不是
AddSingleton
检查
ExecuteAsync
开头是否加了
try/catch
并记录日志 —— 静默失败比报错更难排查 开发环境默认用
Development
环境变量,但某些配置(比如读取 appsettings.Development.json)可能漏掉定时器开关项,建议在代码里硬编码初始延迟(如
await Task.Delay(2000, stoppingToken)
)快速验证入口是否进入 真正麻烦的是跨环境行为差异:Linux 容器里
CancellationToken
响应可能比 Windows 慢几百毫秒,StopAsync 超时设太短会导致任务被粗暴中断。上线前务必在目标环境压测至少两个完整周期。

相关文章