🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀

在这里插入图片描述在这里插入图片描述

C#定时任务的三大"江湖门派"与血泪史

一、System.Threading.Timer:老伙计,但有点"老年痴呆"

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        // 创建一个定时器,设置5秒后执行,之后每5秒执行一次
        // 注意:这里用的是System.Threading.Timer,不是System.Timers.Timer
        // 为啥?因为它是.NET Framework的底层定时器,轻量级但有点"呆"
        Timer timer = new Timer(TimerCallback, null, 5000, 5000);
        
        Console.WriteLine("定时任务已启动,按任意键退出...");
        Console.ReadKey();
        
        // 关键:别忘了停止定时器,否则程序退出后定时器还在后台跑
        // 否则,你可能会在生产环境看到"定时任务线程泄漏"的噩梦
        timer.Dispose();
    }

    // 定时回调方法
    private static void TimerCallback(object state)
    {
        // 这里是你的业务逻辑
        Console.WriteLine($"定时任务执行时间:{DateTime.Now:HH:mm:ss.fff}");
        
        // 重点:这个方法在后台线程执行,不是主线程
        // 所以如果你要更新UI,会抛出跨线程访问异常
        // 解决方案:使用Invoke或BeginInvoke,但这就复杂了
    }
}

墨氏注解:

  • Timer类是.NET Framework的底层定时器,轻量级但功能简单
  • TimerCallback在后台线程执行,不是主线程,所以不能直接操作UI
  • Dispose()必须调用,否则会导致资源泄漏(尤其是长时间运行的应用)
  • 坑点:如果回调方法执行时间超过设定间隔,下一次回调会等当前执行完再开始,可能导致任务堆积

“当年我用这个Timer写定时任务,结果在生产环境跑着跑着,服务器内存像被吸血鬼吸干了一样,排查了三天才发现是没调Dispose()。老铁,别犯我这种低级错误啊!”

二、System.Timers.Timer:更"现代化",但有隐藏的"定时炸弹"

using System;
using System.Timers;

class Program
{
    static Timer timer;
    
    static void Main(string[] args)
    {
        // 创建定时器,设置5秒后执行,之后每5秒执行一次
        timer = new Timer(5000);
        
        // 绑定Elapsed事件,这是定时器的核心
        timer.Elapsed += OnTimedEvent;
        
        // 设置定时器自动触发,否则只触发一次
        timer.AutoReset = true;
        
        // 启动定时器
        timer.Enabled = true;
        
        Console.WriteLine("定时任务已启动,按任意键退出...");
        Console.ReadKey();
        
        // 停止定时器,避免资源泄漏
        timer.Stop();
        timer.Dispose();
    }
    
    // 定时器触发的事件处理方法
    private static void OnTimedEvent(object sender, ElapsedEventArgs e)
    {
        // 这里是你的业务逻辑
        Console.WriteLine($"定时任务执行时间:{e.SignalTime:HH:mm:ss.fff}");
        
        // 重点:Elapsed事件在后台线程触发,同样不能操作UI
        // 而且,如果回调方法执行时间超过间隔,下一次回调会延迟
        // 但不会堆积,因为它是单线程执行
    }
}

墨氏注解:

  • System.Timers.TimerSystem.Threading.Timer的"升级版",面向服务器端应用
  • Elapsed事件在后台线程触发,不能直接操作UI
  • AutoReset属性:true表示自动重复触发,false表示只触发一次
  • 坑点:如果回调方法执行时间超过间隔,下一次回调会延迟,但不会堆积(因为是单线程)

“记得有一次,我写了个定时任务,每天凌晨三点处理数据。结果因为某次数据量大,处理时间超过了5分钟,导致下一次任务延迟了5分钟。产品经理半夜打电话质问我,‘为啥凌晨三点才处理完?’ 我当时就想:‘这不怪我,怪你的数据量太大!’ 但最后还是被骂惨了。”

三、Quartz.NET:定时任务的"瑞士军刀",优雅到哭

using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // 创建调度器工厂
        ISchedulerFactory factory = new StdSchedulerFactory();
        
        // 获取调度器实例
        IScheduler scheduler = await factory.GetScheduler();
        
        // 启动调度器
        await scheduler.Start();
        
        // 定义任务
        IJobDetail job = JobBuilder.Create<SimpleJob>()
            .WithIdentity("job1", "group1")
            .Build();
            
        // 定义触发器,每天凌晨2点执行
        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity("trigger1", "group1")
            .StartNow()
            .WithSimpleSchedule(x => x
                .WithIntervalInMinutes(1440) // 每24小时执行一次
                .RepeatForever())
            .Build();
            
        // 将任务与触发器关联
        await scheduler.ScheduleJob(job, trigger);
        
        Console.WriteLine("定时任务已启动,按任意键退出...");
        Console.ReadKey();
        
        // 关闭调度器
        await scheduler.Shutdown();
    }
}

// 任务实现
public class SimpleJob : IJob
{
    public Task Execute(IJobExecutionContext context)
    {
        // 这里是你的业务逻辑
        Console.WriteLine($"定时任务执行时间:{DateTime.Now:HH:mm:ss.fff}");
        
        // 重点:Execute方法在调度器线程中执行,不是主线程
        // 但Quartz.NET处理了线程管理,你不用操心
        return Task.CompletedTask;
    }
}

墨氏注解:

  • Quartz.NET是一个功能强大的开源调度库,支持Cron表达式
  • Cron表达式:让定时任务变得无比灵活,比如"每天凌晨2点"、“每周一上午10点”、“每月最后一天”
  • 任务在调度器线程中执行,你不用操心线程管理
  • 优势:支持分布式调度、任务持久化、任务重试等高级功能
  • 坑点:需要额外安装NuGet包,配置稍复杂,但值得

“用Quartz.NET之前,我写定时任务就像在用老式打字机,笨重又麻烦。用上它后,我直接把定时任务写成Cron表达式,比如’0 0 2 * * ?‘,表示每天凌晨2点执行。现在,我再也不用担心半夜被报警叫醒了。这玩意儿,简直是定时任务界的’瑞士军刀’!”

四、为什么你的定时任务总在半夜跑崩?——90%开发者踩过的坑

  1. 线程管理不当:定时任务在后台线程执行,如果回调方法执行时间过长,会导致任务堆积,服务器资源耗尽。

    • 解决方案:使用Quartz.NET或确保回调方法执行时间短
  2. 资源泄漏:忘记调用Dispose()Stop(),导致定时器线程一直运行,资源被泄漏。

    • 解决方案:在程序退出前,确保调用Dispose()Stop()
  3. 时间精度问题System.Threading.TimerSystem.Timers.Timer的精度有限,可能无法精确到毫秒。

    • 解决方案:使用Quartz.NET,它提供了更高的时间精度
  4. 任务重复执行:如果回调方法执行时间超过间隔,可能会导致任务重复执行。

    • 解决方案:使用Quartz.NETCronTrigger,避免重复执行
  5. 任务持久化:如果应用重启,定时任务会丢失。

    • 解决方案:使用Quartz.NET的JobStore,将任务持久化到数据库

“我曾经有个定时任务,每5分钟执行一次。结果因为某个数据量大,执行时间超过了5分钟,导致下一次任务延迟了5分钟。然后,下一次任务又因为数据量大,执行时间超过5分钟,导致延迟越来越长。最后,服务器CPU飙升到100%,线上服务直接崩了。现在想想,真是后怕啊!”

五、实战:从老古董到现代工具的进化之路

场景:我们需要一个定时任务,每天凌晨2点处理订单数据。

老古董方案(System.Timers.Timer)

using System;
using System.Timers;

class Program
{
    static Timer timer;
    
    static void Main(string[] args)
    {
        // 创建定时器,设置每天凌晨2点执行
        // 注意:这里需要计算当前时间到凌晨2点的时间差
        DateTime now = DateTime.Now;
        DateTime nextRun = now.Date.AddDays(1).AddHours(2);
        TimeSpan delay = nextRun - now;
        
        timer = new Timer(delay.TotalMilliseconds);
        timer.Elapsed += OnTimedEvent;
        timer.AutoReset = true;
        timer.Enabled = true;
        
        Console.WriteLine("定时任务已启动,按任意键退出...");
        Console.ReadKey();
        
        timer.Stop();
        timer.Dispose();
    }
    
    private static void OnTimedEvent(object sender, ElapsedEventArgs e)
    {
        // 处理订单数据
        Console.WriteLine($"处理订单数据:{DateTime.Now:HH:mm:ss.fff}");
    }
}

墨氏注解

  • 这种方式需要手动计算下一次执行时间,非常麻烦
  • 如果应用重启,定时任务会丢失
  • 无法精确到每天凌晨2点,因为时间计算可能有误差

现代方案(Quartz.NET)

using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        ISchedulerFactory factory = new StdSchedulerFactory();
        IScheduler scheduler = await factory.GetScheduler();
        await scheduler.Start();
        
        // 定义任务
        IJobDetail job = JobBuilder.Create<OrderProcessingJob>()
            .WithIdentity("orderProcessingJob", "orderProcessingGroup")
            .Build();
            
        // 定义触发器,每天凌晨2点执行
        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity("orderProcessingTrigger", "orderProcessingGroup")
            .StartNow()
            .WithCronSchedule("0 0 2 * * ?") // 每天凌晨2点
            .Build();
            
        await scheduler.ScheduleJob(job, trigger);
        
        Console.WriteLine("订单处理定时任务已启动,按任意键退出...");
        Console.ReadKey();
        
        await scheduler.Shutdown();
    }
}

public class OrderProcessingJob : IJob
{
    public Task Execute(IJobExecutionContext context)
    {
        // 处理订单数据
        Console.WriteLine($"处理订单数据:{DateTime.Now:HH:mm:ss.fff}");
        
        // 重点:这里可以做复杂的数据处理
        // 而不用担心线程问题
        return Task.CompletedTask;
    }
}

墨氏注解

  • 使用Cron表达式,精确到每天凌晨2点
  • 任务持久化,应用重启后仍能继续执行
  • 无需手动计算时间,代码更简洁
  • 任务管理更专业,支持任务重试、日志记录等

“以前用老方法,每天都要写一堆时间计算的代码,还容易出错。现在用Quartz.NET,一行Cron表达式搞定,代码简洁多了。而且,即使应用重启,定时任务也不会丢失,这感觉,就像给定时任务装了个’保险丝’,再也不用担心半夜被报警叫醒了。”

定时任务的"进阶之路",你还在等什么?

老码农的总结

  • 对于简单任务,System.Threading.TimerSystem.Timers.Timer足够用
  • 对于复杂任务,尤其是需要精确控制、持久化、分布式调度的场景,Quartz.NET是不二之选

墨氏金句

“别再用老古董了!定时任务不是’能跑就行’,而是’要跑得稳、跑得准、跑得优雅’。Quartz.NET就是你的’定时任务瑞士军刀’,让你的代码优雅到哭。”

墨瑾轩的终极建议

“别再让定时任务在半夜惊醒你了。从今天开始,用Quartz.NET,让你的定时任务跑得稳、跑得准、跑得优雅。记住,代码要优雅,定时任务更要优雅!”

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐