.NET 实现后台任务

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 来优雅地停止任务,确保资源释放。
相关推荐
步步为营DotNet1 小时前
从分代预算到 DATAS:.NET GC 调优到底在调什么
c#·.net·内存管理·gc·datas
淡海水1 小时前
05-03-源码-Boehm GC分配与碎片管理
开发语言·算法·unity·c#·游戏引擎·gc
FuckPatience2 小时前
Windows 窗体设计器不支持项目“xxx,因为它面向不受支持的.NET Core 3.1。建议更新到受支持的.NET 版本。
.netcore
曹牧2 小时前
C#:字符串占位符与值数量不一致
开发语言·数据库·c#
学以智用19 小时前
.NET 10 完整详解
.net
玩泥巴的20 小时前
MAF + 飞书:让大模型干活,但别让它乱干
ai·c#·.net·飞书·agent
cpp_w1 天前
C#基础3- 面向对象中级应用3
后端·c#
步步为营DotNet1 天前
Async 状态机要搬进运行时了:.NET 10 Runtime Async 实测,快的地方快 1.5 倍,贵的地方贵 5 倍
性能优化·c#·.net·async·runtime
hixiong1231 天前
C# 使用 ZXingCpp.Angle 进行条码、二维码识别
开发语言·c#·扫码