定时任务之BackgroundService的详细教程

定时任务之BackgroundService的详细教程

一、BackgroundService是什么

在 .NET 中,BackgroundService 是实现后台定时任务的标准方式。它继承自 IHostedService,能够随应用程序启动而自动运行,并在应用关闭时优雅停止。

官方解释:

BackgroundService 是 .NET 提供的一个抽象基类,位于 Microsoft.Extensions.Hosting 命名空间,专门用来简化长时间运行的后台任务的实现。

它实现了 IHostedService 接口,并封装了 StartAsync 和 StopAsync 的常规处理,你只需要重写一个方法:

ExecuteAsync(CancellationToken stoppingToken):在这里编写你的后台逻辑,通常是一个循环,直到 stoppingToken 发出取消信号时退出。

二、 怎么实现定时任务

  1. 核心实现步骤
    第一步:创建 定时任务服务类
    继承 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();
        }

    }
}
  1. 常见陷阱与解决方案
问题 原因 解决方案
服务启动但不执行‌ 未重写 ExecuteAsync 或注册顺序错误 确保重写 ExecuteAsync 且包含异步等待逻辑;在 Program.cs 中显式调用 AddHostedService()。
内存泄漏/资源未释放‌ 未正确使用 using 或 Dispose 使用 using var timer 和 using var scope 确保资源自动释放。
多实例重复执行‌ 多个服务器实例同时运行同一任务 单机 Timer 无法解决分布式问题。需引入‌分布式锁‌(如 Redis Lock、数据库应用锁 sp_getapplock)确保同一时刻只有一个实例执行。
任务堆积/雪崩‌ 任务执行时间超过定时间隔 PeriodicTimer 会等待上一轮完成。若任务耗时过长,需优化性能或调整间隔。对于关键任务,建议设置超时控制(如 WaitAsync)。

相关推荐
E等于MC平方1 小时前
用 Rust 写一个工业级 POSP 支付系统
后端·rust·消费·8583·交易·posp·银联
程序员阿明2 小时前
spring boot + vue3 实现RSA加密解密
java·spring boot·后端
明月_清风2 小时前
Redis 数据类型全景解析:从基础到高阶,一文掌握九大核心结构与应用场景
redis·后端
明月_清风2 小时前
深入浅出 Elasticsearch:核心概念、工具链与底层原理全解析
后端·elasticsearch
彭于晏Yan2 小时前
HttpServletRequest 如何读取JSON请求体
spring boot·后端·json
小李云雾2 小时前
慧校坊-二手校园交易平台-------项目总结
数据库·后端·程序人生·fastapi·项目
weixin_428005302 小时前
C#调用 AI学习从0开始-第1阶段(基础与工具)-第3天FewShot少样本测试
人工智能·c#
思麟呀2 小时前
在C++基础上理解CSharp-1
开发语言·c++·c#