IHostedService 和 BackgroundService
在 .NET Core 中实现后台任务,常用的方式是使用托管服务 (Hosted Services)。这通过实现 IHostedService 接口或继承其抽象基类 BackgroundService来完成。这种方式能确保后台任务与应用程序的生命周期(启动、停止、重启)紧密绑定,避免任务丢失或资源泄露。
IHostedService定义了两个方法 StartAsync 和 StopAsync。宿主会在应用启动时调用 StartAsync,在应用关闭时调用 StopAsync。
BackgroundService是 IHostedService 的常用基类,简化了长时间运行任务的实现。你只需重写 ExecuteAsync 方法。它会自动处理取消令牌(CancellationToken),以便在应用关闭时停止任务。
BackgroundService 实现长期运行的后台任务
BackgroundService 实现长期运行的后台任务,适用于需要持续运行、轮询或处理队列的任务。
PS:
- 不要在 StartAsync 中执行长时间运行的循环,否则会导致应用启动阻塞。
- 在 ExecuteAsync 中使用 while (!stoppingToken.IsCancellationRequested) 循环。
- 使用 await Task.Delay(..., stoppingToken) 代替 Thread.Sleep,以支持取消操作。
cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
public class MyBackgroundWorker : BackgroundService
{
private readonly ILogger<MyBackgroundWorker> _logger;
public MyBackgroundWorker(ILogger<MyBackgroundWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Background Worker is starting.");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 执行你的后台逻辑
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
// 模拟耗时操作,同时监听取消信号
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (OperationCanceledException)
{
// 当取消令牌被触发时,正常退出循环
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred in background task.");
// 可以选择重试或记录错误后继续
}
}
_logger.LogInformation("Background Worker is stopping.");
}
}
注册服务 (Program.cs):
cs
var builder = WebApplication.CreateBuilder(args);
// 注册后台服务
builder.Services.AddHostedService<MyBackgroundWorker>();
var app = builder.Build();
app.Run();
使用 Timer 实现定时任务
如果任务是周期性的(例如每几分钟执行一次),可以使用 System.Threading.Timer。
cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
public class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger<TimedHostedService> _logger;
private Timer? _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service running.");
// 立即执行一次,然后每 5 秒执行一次
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
_logger.LogInformation("Timed Hosted Service is working.");
// 在这里执行具体的业务逻辑
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
// 禁用计时器
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
使用队列处理后台任务 (生产者-消费者模式)
对于由 HTTP 请求触发的后台任务(如发送电子邮件、生成报表),可以建议使用队列解耦。
定义队列接口和实现:
cs
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new();
private SemaphoreSlim _signal = new(0);
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
{
if (workItem == null) throw new ArgumentNullException(nameof(workItem));
_workItems.Enqueue(workItem);
_signal.Release();
}
public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem!;
}
}
创建消费服务的后台工作器
cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
public class QueuedHostedService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred executing {workItem}.");
}
}
}
}
注册服务并在 Controller 中使用
cs
// Program.cs
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
builder.Services.AddHostedService<QueuedHostedService>();
// Controller
[HttpPost("send-email")]
public IActionResult SendEmail([FromBody] EmailRequest request)
{
_taskQueue.QueueBackgroundWorkItem(async token =>
{
// 执行发送邮件逻辑
await _emailService.SendAsync(request.To, request.Body, token);
});
return Accepted();
}
Hangfire
开箱即用的任务处理平台。不仅负责调度,还负责任务的持久化、重试、监控和管理。
内置 Dashboard。提供的 Web 界面,可实时查看排队中、正在运行、成功、失败的任务,支持手动重试、删除任务。
强持久化。默认将任务状态存入 SQL Server、Redis、PostgreSQL等。服务重启后,未执行的任务不会丢失,会自动恢复执行。
自动重试。内置的重试机制,可配置重试次数和间隔。失败任务可在 Dashboard 一键重试。
支持即时任务 (Fire-and-forget)、延迟任务 (Delayed)、定时/循环任务 (Recurring)、连续任务链。
复杂的相对低。只需安装 NuGet 包,配置存储连接字符串即可嵌入 ASP.NET Core应用。无需 Windows 服务。
如果希望快速集成、需要看到任务执行状态、担心任务丢失、需要失败自动重试或手动重推。Hangfire 是优先选择。
Hangfire 使用
安装 NuGet 包,根据你选择的存储介质安装相应的包。以 SQL Server 为例:
dotnet add package Hangfire
dotnet add package Hangfire.SqlServer
如果使用 Redis: dotnet add package Hangfire.Redis.StackExchange
配置服务 (Program.cs)
cs
var builder = WebApplication.CreateBuilder(args);
// 1. 添加 Hangfire 服务并配置存储
builder.Services.AddHangfire(config =>
{
// 使用 SQL Server 存储
config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"));
// 可选:配置 Dashboard 选项
// config.UseDashboardMetrics();
});
// 2. 添加 Hangfire Server(负责执行任务)
builder.Services.AddHangfireServer();
var app = builder.Build();
// 3. 启用 Dashboard 中间件 (默认路径 /hangfire)
// 注意:生产环境务必添加身份验证授权,防止未授权访问
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireCustomAuthorizationFilter() } // 自定义授权过滤器
});
app.Run();
默认情况下: UseHangfireDashboard 允许本地访问,但在生产环境中,必须实现 IDashboardAuthorizationFilter 接口来限制访问权限,否则任何人都可以查看和执行你的后台任务。
定义任务方法 ,Hangfire 可以直接调用静态方法或实例方法。
cs
public class EmailService
{
public void SendEmail(string to, string subject)
{
Console.WriteLine($"Sending email to {to}: {subject}");
// 模拟耗时操作
Thread.Sleep(2000);
}
}
enqueue 任务 在 Controller 或其他服务中注入 IBackgroundJobClient 或直接使用静态类 BackgroundJob。
cs
[ApiController]
[Route("[controller]")]
public class TaskController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobs;
public TaskController(IBackgroundJobClient backgroundJobs)
{
_backgroundJobs = backgroundJobs;
}
[HttpPost("send")]
public IActionResult Send()
{
// 1. 即时任务 (Fire-and-forget)
// 任务会被存入数据库,立即由后台线程拾取执行
_backgroundJobs.Enqueue<EmailService>(x => x.SendEmail("user@example.com", "Hello"));
// 2. 延迟任务 (Delayed) - 1分钟后执行
_backgroundJobs.Schedule<EmailService>(x => x.SendEmail("user@example.com", "Delayed"), TimeSpan.FromMinutes(1));
// 3. 循环任务 (Recurring) - 每天凌晨2点执行
// 通常在 Startup 或 Program 初始化时注册,而不是在请求中
// RecurringJob.AddOrUpdate<EmailService>("daily-report", x => x.SendEmail("admin@co.com", "Report"), Cron.Daily);
return Ok("Task queued.");
}
}
自动重试
cs
using Hangfire.Common;
using Hangfire.States;
using Hangfire.Storage;
public class ResilientService
{
[AutomaticRetry(Attempts = 3, OnAttemptFailed = "LogFailure")]
public void UnreliableOperation()
{
// 可能会抛出异常的操作
if (DateTime.Now.Second % 2 == 0)
throw new Exception("Temporary failure");
Console.WriteLine("Success");
}
public void LogFailure(AttemptContext context)
{
Console.WriteLine($"Attempt {context.AttemptNumber} failed: {context.Exception.Message}");
}
}
小结:
- 推荐 Hangfire。它的学习曲线平缓,Dashboard 能极大降低运维排查难度,且自动重试机制能提高系统的健壮性。
- 记得在生产环境配置 Dashboard 的身份验证。
- 选择合适的存储:SQL Server 适合传统企业应用,Redis 适合高并发、高性能场景。
Quartz.NET
Quartz.NET 更加底层,需要明确定义 Job (做什么)、Trigger (何时做)和 Scheduler(调度器)。
安装 NuGet 包
dotnet add package Quartz
dotnet add package Quartz.Extensions.Hosting # 用于简化 ASP.NET Core 集成
定义 Job 类
必须实现 IJob 接口。
cs
using Quartz;
public class PrintTimeJob : IJob
{
private readonly ILogger<PrintTimeJob> _logger;
public PrintTimeJob(ILogger<PrintTimeJob> logger)
{
_logger = logger;
}
public Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Quartz Job executed at: {Time}", DateTime.Now);
// 获取传递的参数
var jobDataMap = context.JobDetail.JobDataMap;
var message = jobDataMap.GetString("message");
_logger.LogInformation("Message: {Message}", message);
return Task.CompletedTask;
}
}
配置 Quartz (Program.cs)
使用 Quartz.Extensions.Hosting 可以极大地简化配置。
cs
var builder = WebApplication.CreateBuilder(args);
// 添加 Quartz 服务
builder.Services.AddQuartz(q =>
{
// 配置持久化 (可选,默认是内存 RAMJobStore)
// q.UsePersistentStore(s => s.UseProperties = true);
// 配置线程池
q.UseSimpleTypeLoader();
q.UseInMemoryStore(); // 开发环境用内存,生产环境建议用 SQL Server 或 Redis
});
// 添加后台服务来启动 Scheduler
builder.Services.AddQuartzHostedService(options =>
{
// 等待所有任务完成后再关闭应用
options.WaitForJobsToComplete = true;
});
var app = builder.Build();
app.Run();
调度任务 ,以在 Program.cs 启动时配置静态任务,或者通过注入 IScheduler 动态添加。
在 Program.cs 中配置静态调度
cs
builder.Services.AddQuartz(q =>
{
q.AddJob<PrintTimeJob>(opts => opts.WithIdentity("printTimeJob"));
q.AddTrigger(opts => opts
.ForJob("printTimeJob")
.WithIdentity("printTimeTrigger")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever()));
});
在代码中动态调度 (注入 IScheduler)
cs
[ApiController]
[Route("[controller]")]
public class QuartzController : ControllerBase
{
private readonly IScheduler _scheduler;
public QuartzController(IScheduler scheduler)
{
_scheduler = scheduler;
}
[HttpPost("schedule")]
public async Task<IActionResult> ScheduleJob()
{
// 定义 Job
var job = JobBuilder.Create<PrintTimeJob>()
.WithIdentity("dynamicJob", "group1")
.UsingJobData("message", "Hello from Dynamic Job")
.Build();
// 定义 Trigger (例如:5秒后执行一次)
var trigger = TriggerBuilder.Create()
.WithIdentity("dynamicTrigger", "group1")
.StartAt(DateTimeOffset.UtcNow.AddSeconds(5))
.WithSimpleSchedule(x => x.WithRepeatCount(0)) // 只执行一次
.Build();
await _scheduler.ScheduleJob(job, trigger);
return Ok("Job scheduled.");
}
}
小结:
- 选择 Quartz.NET 。如果你需要处理复杂的日历排除逻辑(如"工作日每天早上9点,但排除法定节假日"),Quartz 的
Calendar功能更强大。 - 如果需要集群部署,Quartz 的原生集群支持非常成熟,但配置较复杂。
总结
- 简单定时/长期运行: 使用 BackgroundService。
- 请求触发的异步任务: 使用 BackgroundService + Channel 或自定义队列。
- 复杂调度/持久化/重试: 使用 Quartz.NET 或 Hangfire。
- 避免不要使用 Task.Run 而不等待(Fire-and-forget),因为这会导致任务在应用重启时丢失,且无法捕获异常。
- 生命周期管理: 始终利用 CancellationToken 来优雅地停止任务,确保资源释放。