C#中让Timer在特定时间点触发

网友Behrooz Karjoo咨询:

我的应用程序需要做一个 事件触发 的功能,它需要每天定时执行,比如说当天的 16点,我现在的做法是使用一个 timer 按秒轮询判断当前是否为 16:00, 虽然可以玩得转,但我想能不能实现那种 16:00 自动触发回调函数的模式,而不是现在无时无刻的轮询。

网友noontz回答:

其实很简单,计算触发时间与当前时间的差值,然后将 差值 作为精确的延迟时间,带入到 Task.Delay 中即可,外面再套个 while(true) ,下面是我封装的代码。.

/// <summary>
/// Utility class for triggering an event every 24 hours at a specified time of day
/// </summary>
public class DailyTrigger : IDisposable
{
   /// <summary>
   /// Time of day (from 00:00:00) to trigger
   /// </summary>
   TimeSpan TriggerHour { get; }

   /// <summary>
   /// Task cancellation token source to cancel delayed task on disposal
   /// </summary>
   CancellationTokenSource CancellationToken { get; set; }

   /// <summary>
   /// Reference to the running task
   /// </summary>
   Task RunningTask { get; set; }

   /// <summary>
   /// Initiator
   /// </summary>
   /// <param name="hour">The hour of the day to trigger</param>
   /// <param name="minute">The minute to trigger</param>
   /// <param name="second">The second to trigger</param>
   public DailyTrigger(int hour, int minute = 0, int second = 0)
   {
       TriggerHour = new TimeSpan(hour, minute, second);
       CancellationToken = new CancellationTokenSource();
       RunningTask = Task.Run(async () => 
       {
           while (true)
           {
               var triggerTime = DateTime.Today + TriggerHour - DateTime.Now;
               if (triggerTime < TimeSpan.Zero)
                   triggerTime = triggerTime.Add(new TimeSpan(24, 0, 0));
               await Task.Delay(triggerTime, CancellationToken.Token);
               OnTimeTriggered?.Invoke();
           }
       }, CancellationToken.Token);
   }

   /// <inheritdoc/>
   public void Dispose()
   {
       CancellationToken?.Cancel();
       CancellationToken?.Dispose();
       CancellationToken = null;
       RunningTask?.Dispose();
       RunningTask = null;
   }

   /// <summary>
   /// Triggers once every 24 hours on the specified time
   /// </summary>
   public event Action OnTimeTriggered;

   /// <summary>
   /// Finalized to ensure Dispose is called when out of scope
   /// </summary>
   ~DailyTrigger() => Dispose();
}

然后像下面这样使用。

void Main()
{
    var trigger = new DailyTrigger(16); // every day at 4:00pm
    trigger.OnTimeTriggered += () => 
    {
        // Whatever
    };  
    Console.ReadKey();
}

点评:思路是个好思路,不过在正式的项目开发中,建议还是用 Quartz.NET 或者 HangFire 这种专业的调度框架,毕竟它支持强大的 Cron 表达式。

RecurringJob.AddOrUpdate(() => MyMethod(), "* 16 * * *");