C#中的System.Threading.Timer似乎无法正常工作,它每3秒运行非常快速。

115

我有一个计时器对象。我希望它每分钟运行一次。具体来说,它应该运行一个名为OnCallBack的方法,并在OnCallBack方法正在运行时处于非活动状态。一旦OnCallBack方法完成,它(OnCallBack)会重新启动计时器。

这是我目前拥有的:

private static Timer timer;

private static void Main()
{
    timer = new Timer(_ => OnCallBack(), null, 0, 1000 * 10); //every 10 seconds
    Console.ReadLine();
}

private static void OnCallBack()
{
    timer.Change(Timeout.Infinite, Timeout.Infinite); //stops the timer
    Thread.Sleep(3000); //doing some long operation
    timer.Change(0, 1000 * 10);  //restarts the timer
}

然而,它似乎没有起作用。它每3秒非常快地运行一次。即使我提高了周期(1000*10),它似乎对1000 * 10视而不见。

我做错了什么?


12
根据Timer.Change的说明:“如果dueTime为零(0),则立即调用回调方法。” 看起来它确实是零。 - Damien_The_Unbeliever
2
是的,但那又怎样?还有一个句号。 - Alan Coromano
10
那又怎样,就算有一个句号呢?引用的句子并没有关于句号值的声明。它只是说:“如果这个值为零,我将立即调用回调函数”。 - Damien_The_Unbeliever
3
有趣的是,如果你将dueTime和period都设置为0,计时器将每秒运行一次并立即启动。 - Kelvin
5个回答

247

这不是使用 System.Threading.Timer 的正确方式。当你实例化 Timer 时,几乎总是应该执行以下操作:

_timer = new Timer( Callback, null, TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );

这将指示计时器在间隔时间到达后仅触发一次。然后在您的回调函数中,只有在工作完成后才更改计时器,而不是在之前。示例:

private void Callback( Object state )
{
    // Long running operation
   _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );
}
因此,由于不存在并发,所以不需要加锁机制。计时器将在下一个间隔已经过去的时间 + 长时间运行操作的时间之后触发下一个回调。
如果您需要以确切的N毫秒运行计时器,则建议使用Stopwatch测量长时间运行操作的时间,然后适当调用Change方法:
private void Callback( Object state )
{
   Stopwatch watch = new Stopwatch();

   watch.Start();
   // Long running operation

   _timer.Change( Math.Max( 0, TIME_INTERVAL_IN_MILLISECONDS - watch.ElapsedMilliseconds ), Timeout.Infinite );
}

强烈推荐使用CLR的.NET开发者读一下Jeffrey Richter的书 - 通过C#学习CLR,尽快阅读。那里详细解释了定时器和线程池。


6
我不同意那段代码中的 private void Callback( Object state ) { // Long running operation _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite ); }。在操作完成之前,Callback 可能会被再次调用。 - Alan Coromano
2
我的意思是“长时间运行操作”可能需要比“TIME_INTERVAL_IN_MILLISECONDS”更长的时间。那么会发生什么? - Alan Coromano
33
回调将不会再被调用,这就是关键点。这就是为什么我们将Timeout.Infinite作为第二个参数传递的原因。这基本上意味着定时器不再触发Tick事件。然后在完成操作后重新安排定时器以再次触发Tick事件。 - Ivan Zlatanov
我是一个线程编程的新手 - 你认为使用ThreadPool可以实现这个功能吗?我在考虑这样一种情况:在特定的时间间隔内,生成一个新的线程来执行任务,然后在完成后将其归入线程池。 - user677526
2
System.Threading.Timer是一个线程池计时器,它在线程池上执行回调,而不是在专用线程上执行。计时器完成回调例程后,执行回调的线程将返回到池中。 - Ivan Zlatanov
显示剩余3条评论

14

没有必要停止计时器,参考这篇文章中的好方法

"你可以让计时器继续触发回调方法,但将不可重入的代码包裹在 Monitor.TryEnter/Exit 中。在这种情况下,不需要停止/重启计时器,重叠的调用将不能获得锁并立即返回。"

private void CreatorLoop(object state) 
 {
   if (Monitor.TryEnter(lockObject))
   {
     try
     {
       // Work here
     }
     finally
     {
       Monitor.Exit(lockObject);
     }
   }
 }

我需要精确停止计时器,这与我的情况不符。 - Alan Coromano
你是在尝试防止回调被多次调用吗?如果不是,那么你想要实现什么? - Ivan Leonenko
  1. 防止回调函数被多次调用。
  2. 防止执行过多次。
- Alan Coromano
这就是它的作用。只要在对象被锁定后立即返回if语句,#2并不会有太多开销,尤其是如果你有如此大的间隔时间。 - Ivan Leonenko
1
这并不保证代码在上一次执行后不到<interval>就被调用(定时器的新滴答声可能会在上一个滴答声释放锁之后的微秒内触发)。这取决于这是否是一个严格要求(从问题描述中并不完全清楚)。 - Marco Mp

9

使用 System.Threading.Timer 是必须的吗?

如果不是,System.Timers.Timer 有方便的 Start()Stop() 方法(还有可以设置为 false 的 AutoReset 属性,因此不需要 Stop(),而只需执行后调用 Start())。


4
是的,但这可能是一个真正的需求,或者只是因为计时器是最常用的而被选择。遗憾的是,.NET有大量计时器对象,其中90%重叠但仍然(有时微妙地)不同。当然,如果这是一个需求,这个解决方案就完全不适用。 - Marco Mp
4
根据文档:Systems.Timer 类仅在 .NET Framework 中可用,不包含在 .NET Standard Library 中,也不适用于其他平台,例如.NET Core或通用Windows平台。 在这些平台上以及为了在所有.NET平台上实现可移植性,您应该使用 System.Threading.Timer 类。请注意,两个类之间的用法略有不同。 - NotAgain

3
我只会这样做:
private static Timer timer;
 private static void Main()
 {
   timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
   Console.ReadLine();
 }

  private static void OnCallBack()
  {
    timer.Dispose();
    Thread.Sleep(3000); //doing some long operation
    timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
  }

忽略周期性参数,因为你试图自己控制周期。由于你一直在 dueTime 参数中指定 0,所以你的原始代码运行得尽可能快。来自 Timer.Change 的说明如下:

如果 dueTime 为零(0),则会立即调用回调方法。


2
需要调用Dispose释放定时器吗?你为什么不使用Change()方法呢? - Alan Coromano
24
每次处理定时器都是完全不必要且错误的。 - Ivan Zlatanov
@IvanZlatanov 你能否就此提供更多深入的见解?当我们调用dispose时,对象将从内存中清除,这是正确的吧?我只看到可能会影响解决方案的一些额外过程。只是在大声思考... - Sudhakar Chavali
1
@SudhakarChavali 当你使用完对象后,应该始终将其处理掉。在这个例子中,因为计时器具有可以启用/禁用实例的Change方法,所以处理它并创建新的是不必要的。 - Ivan Zlatanov

0
 var span = TimeSpan.FromMinutes(2);
 var t = Task.Factory.StartNew(async delegate / () =>
   {
        this.SomeAsync();
        await Task.Delay(span, source.Token);
  }, source.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

source.Cancel(true/or not);

// or use ThreadPool(whit defaul options thread) like this
Task.Start(()=>{...}), source.Token)

如果你喜欢在里面使用一些循环线程...

public async void RunForestRun(CancellationToken token)
{
  var t = await Task.Factory.StartNew(async delegate
   {
       while (true)
       {
           await Task.Delay(TimeSpan.FromSeconds(1), token)
                 .ContinueWith(task => { Console.WriteLine("End delay"); });
           this.PrintConsole(1);
        }
    }, token) // drop thread options to default values;
}

// And somewhere there
source.Cancel();
//or
token.ThrowIfCancellationRequested(); // try/ catch block requred.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接