c#任务调度需按场景选工具:简单定时用timer,跨进程高可靠用hangfire,轻量后台用ihostedservice,复杂工作流才选quartz.net。

没有“一篇就够了”的万能方案,C# 任务调度必须按场景选工具:简单定时用 Timer,跨进程/高可靠用 Hangfire,轻量后台用 IHostedService,复杂工作流才考虑 Quartz.NET。
什么时候该用 System.Threading.Timer?
适合内存内、短周期、不关心失败重试、应用生命周期一致的场景(比如每 5 秒刷一次缓存状态)。
- 它不跟踪任务执行状态,
Timer回调抛异常会静默吞掉,必须手动try/catch - 回调在
ThreadPool线程运行,不能直接 await 异步方法;要 await 得改用Task.Run+async void(不推荐)或封装成Task后用ContinueWith - 如果应用关闭时没
Dispose(),可能引发内存泄漏或残留线程
示例关键点:
var timer = new Timer(_ => {
try {
DoWorkAsync().Wait(); // 同步等待(简单场景可接受)
} catch (Exception ex) {
Log.Error(ex);
}
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
// 应用退出前务必调用 timer.Dispose()
为什么 IHostedService 比裸写 Timer 更安全?
它把调度逻辑绑定到 .NET Core / .NET 5+ 的主机生命周期,天然支持启动/停止协调、依赖注入、日志集成。
-
StartAsync中启动定时器,StopAsync中主动取消CancellationToken并等待任务结束,避免强制中断导致数据不一致 - 可直接注入
ILogger、IServiceScopeFactory,不用手动管理服务生命周期 - 不适合长耗时任务——默认
StopAsync只有 5 秒超时,超时后进程会被杀;大任务得拆解或移交后台队列
关键不是“能不能跑”,而是“关机时会不会丢数据”。
Hangfire 和 Quartz.NET 到底差在哪?
核心区别不在功能多寡,而在存储和部署模型:
-
Hangfire依赖 SQL Server / Redis 存储作业和状态,自带 Web Dashboard,部署即用,适合中小团队快速上线带 UI 的后台任务(如邮件发送、报表生成) -
Quartz.NET本身不绑定存储,可配 RAMJobStore(仅内存)、ADO.NET(需建表)、MongoDB 等;API 更底层、更灵活,但配置复杂,Dashboard 需额外集成Quartzmin - 两者都支持分布式锁,但 Hangfire 的 Redis 模式对网络分区更敏感;Quartz 的 ADO.NET 模式在 SQL Server 高可用下更稳
别只看“支持 Cron 表达式”——如果任务失败后需要人工干预重试,Hangfire 的 Dashboard 点几下就行;Quartz 得写代码查数据库再调 API。
异步方法调度最容易踩的坑
几乎所有新手都栽在“以为 async void 能当定时回调用”上:
-
async void无法被捕捉异常,也无法 await 等待完成,Timer或IHostedService里绝对不要这么写 - 正确做法是:在同步回调里启动
Task.Run(async () => await DoWorkAsync()),或用FireAndForget模式(需自行处理异常和日志) - 如果任务本身可能并发执行(比如每 10 秒触发,但单次执行耗时 15 秒),必须加锁(
SemaphoreSlim)或跳过本次,否则会堆积
真正难的从来不是“怎么启动一个任务”,而是“怎么确保它只运行一次、失败了有记录、重启后能续上、资源满了不拖垮主线程”。这些细节,文档不会主动告诉你。











