【问题标题】:Stopping timer in its callback method在其回调方法中停止计时器
【发布时间】:2010-12-14 13:07:57
【问题描述】:

我有一个 System.Threading.Timer,它每 10 毫秒调用一次相应的事件处理程序(回调)。该方法本身不可重入,有时可能需要超过 10 毫秒。因此,我想在方法执行期间停止计时器。

代码:

private Timer _creatorTimer;

// BackgroundWorker's work
private void CreatorWork(object sender, DoWorkEventArgs e) {
      _creatorTimer = new Timer(CreatorLoop, null, 0, 10);

      // some other code that worker is doing while the timer is active
      // ...
      // ...
}

private void CreatorLoop(object state) {
      // Stop timer (prevent reentering)
      _creatorTimer.Change(Timeout.Infinite, 0);

      /*
          ... Work here
      */

      // Reenable timer
      _creatorTimer.Change(10, 0);
} 

MSDN 声明回调方法在与线程池不同的线程中被调用(每次定时器触发)。这意味着,如果我在方法中的第一件事中停止计时器,它仍然不一定会阻止计时器触发并在第一个有机会停止计时器之前运行该方法的另一个实例。

是否应该锁定计时器(甚至是不可重入方法本身)? 在执行其回调(和不可重入)方法期间防止计时器触发的正确方法是什么?

【问题讨论】:

标签: c# callback timer reentrancy


【解决方案1】:

我在 System.Timers.Timer 遇到过类似的情况,其中经过的事件是从线程池执行的,需要可重入。

我用这个方法来解决这个问题:

private void tmr_Elapsed(object sender, EventArgs e)
{
    tmr.Enabled = false;
    // Do Stuff
    tmr.Enabled = true;
}

根据您正在做的事情,您可能需要考虑 System.Timers.Timer,这是来自MSDN 的一个很好的总结

                                         System.Windows.Forms    System.Timers         System.Threading  
Timer event runs on what thread?         UI thread               UI or worker thread   Worker thread
Instances are thread safe?               No                      Yes                   No
Familiar/intuitive object model?         Yes                     Yes                   No
Requires Windows Forms?                  Yes                     No                    No
Metronome-quality beat?                  No                      Yes*                  Yes*
Timer event supports state object?       No                      No                    Yes
Initial timer event can be scheduled?    No                      No                    Yes
Class supports inheritance?              Yes                     Yes                   No

* Depending on the availability of system resources (for example, worker threads)            

【讨论】:

  • 我相信这实际上并没有解决这个问题。好吧,在 99.9% 的情况下可能是这样,但是如果系统在下一个计时器的 Elapsed 事件触发之前没有给您的事件处理程序提供处理器时间,则两个不同的线程可能会并行执行该方法。
  • 好点!您总是可以将它与 jsw 之类的锁定解决方案结合使用
【解决方案2】:

您可以让计时器继续触发回调方法,但将不可重入代码包装在 Monitor.TryEnter/Exit 中。在这种情况下无需停止/重新启动计时器;重叠调用不会立即获取锁并返回。

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

【讨论】:

  • +1 我从来没有真正想过 TryEnter 的用途;这很有趣。
  • 这似乎起到了作用。两个或多个线程可以进入该方法,但只有一个线程会真正起作用。我还在 Monitor.TryEnter() 之后停止了计时器,因此它在执行期间根本不会触发(不需要触发),以防万一执行时间远大于计时器的周期。定时器在工作方法完成后重新启动。
  • 取决于应用程序所有者的要求。在您的解决方案中,计时器将每 10 秒调用一次,因此如果不输入,下一次尝试将持续 20 秒。当我们停止/启动计时器时,处理程序的下一次调用将在之前完成处理程序后 10 秒。
【解决方案3】:

几个可能的解决方案:

  • 在另一个等待事件的线程委托中完成真正的工作。计时器回调仅发出事件信号。工作线程不能重新进入,因为它是一个单线程,仅在事件发出信号时才工作。计时器是可重入的,因为它所做的只是向事件发出信号(看起来有点迂回和浪费,但它会起作用)
  • 创建的定时器只有一个启动超时,没有周期性超时,所以它只会触发一次。计时器回调将处理该计时器对象并在完成其工作后创建一个新对象,该对象也只会触发一次。

您可以通过使用原始计时器对象的 Change() 方法来管理选项 #2 而无需处理/创建新对象,但我不确定使用 Change() 调用第一次超时后的新开始超时。这值得一两次测试。

编辑:


我进行了测试 - 将计时器作为可重新启动的一次性操作似乎工作得很好,而且比其他方法简单得多。以下是一些基于您的示例代码作为起点(一些细节可能已更改以使其在我的机器上编译):

private Timer _creatorTimer;

// BackgroundWorker's work
private void CreatorWork(object sender, EventArgs e) {
    // note: there's only a start timeout, and no repeat timeout
    //   so this will fire only once
    _creatorTimer = new Timer(CreatorLoop, null, 1000, Timeout.Infinite);

    // some other code that worker is doing while the timer is active
    // ...
    // ...
}

private void CreatorLoop(object state) {
    Console.WriteLine( "In CreatorLoop...");
    /*
        ... Work here
    */
    Thread.Sleep( 3000);

    // Reenable timer
    Console.WriteLine( "Exiting...");

    // now we reset the timer's start time, so it'll fire again
    //   there's no chance of reentrancy, except for actually
    //   exiting the method (and there's no danger even if that
    //   happens because it's safe at this point).
    _creatorTimer.Change(1000, Timeout.Infinite);
}

【讨论】:

  • 这似乎可行,是的,但是当方法的结果很多(许多 if-else 分支和异常)时,代码变得不那么干净。但是在性能方面似乎是一个很好的解决方案,因为没有使用同步机制。除此之外,如果有其他方法/线程/计时器可以尝试进入此方法,这将不起作用。然后,当然,我们正在重新进入不可重入方法,这是监视器工作得更好的地方。无论如何,感谢您的解决方案和测试。这是个好主意。
  • 与使用互斥体相比,代码的复杂性不再是问题 - 只需将代码包装在 try/finally 中,或者简单地调用另一个具有复杂性的例程并让计时器回调例程成为一个简单的“调用然后重置计时器”。如果回调将被多个计时器使用,那么是的,这种技术将不起作用,您将需要一个真正的同步对象。我发现一个定时器回调被多个定时器使用是非常罕见的——尤其是当回调非常复杂时(这通常意味着定时器回调是为一个非常特定的目的而设计的)。
  • 您可以使用 System.Timers.Timer 实现相同的行为,我认为这更容易。见stackoverflow.com/questions/7055820/non-reentrant-timers
【解决方案4】:

我使用提供原子操作的 Interlocked 来执行此操作,并通过 CompareExchange 确保一次只有一个线程进入临界区:

private int syncPoint = 0;

private void Loop()
    {
        int sync = Interlocked.CompareExchange(ref syncPoint, 1, 0);
         //ensures that only one timer set the syncPoint to  1 from 0
        if (sync == 0)
        {
            try
            {
               ...
            }
            catch (Exception pE)
            {
               ...  
            }
            syncPoint = 0;
        }

    }

【讨论】:

    【解决方案5】:
        //using Timer with callback on System.Threading namespace
        //  Timer(TimerCallback callback, object state, int dueTime, int period);
        //      TimerCallback: delegate to callback on timer lapse
        //      state: an object containig information for the callback
        //      dueTime: time delay before callback is invoked; in milliseconds; 0 immediate
        //      period: interval between invocation of callback; System.Threading.Timeout.Infinity to disable
        // EXCEPTIONS:
        //      ArgumentOutOfRangeException: negative duration or period
        //      ArgumentNullException: callback parameter is null 
    
        public class Program
        {
            public void Main()
            {
                var te = new TimerExample(1000, 2000, 2);
            }
        }
    
        public class TimerExample
        {
            public TimerExample(int delayTime, int intervalTime, int treshold)
            {
                this.DelayTime = delayTime;
                this.IntervalTime = intervalTime;
                this.Treshold = treshold;
                this.Timer = new Timer(this.TimerCallbackWorker, new StateInfo(), delayTime, intervalTime);
            }
    
            public int DelayTime
            {
                get;
                set;
            }
    
            public int IntervalTime
            {
                get;
                set;
            }
    
            public Timer Timer
            {
                get;
                set;
            }
    
            public StateInfo SI
            {
                get;
                set;
            }
    
            public int Treshold
            {
                get;
                private set;
            }
    
            public void TimerCallbackWorker(object state)
            {
                var si = state as StateInfo;
    
                if (si == null)
                {
                    throw new ArgumentNullException("state");
                }
    
                si.ExecutionCounter++;
    
                if (si.ExecutionCounter > this.Treshold)
                {
                    this.Timer.Change(Timeout.Infinite, Timeout.Infinite);
                    Console.WriteLine("-Timer stop, execution reached treshold {0}", this.Treshold);
                }
                else
                {
                    Console.WriteLine("{0} lapse, Time {1}", si.ExecutionCounter, si.ToString());
                }
            }
    
            public class StateInfo
            {
                public int ExecutionCounter
                {
                    get;
                    set;
                }
    
                public DateTime LastRun
                {
                    get
                    {
                        return DateTime.Now;
                    }
                }
    
                public override string ToString()
                {
                    return this.LastRun.ToString();
                }
            }
        }
    
        // Result:
        // 
        //  1 lapse, Time 2015-02-13 01:28:39 AM
        //  2 lapse, Time 2015-02-13 01:28:41 AM
        //  -Timer stop, execution reached treshold 2
        // 
    

    【讨论】:

    • 我建议使用代码格式化程序或其他东西来整理该代码,重新。间距,格式等。这可能很棒,但第一印象很重要,如果我不想使用看起来像那样的代码,那也是我的第一印象。
    • 这是一个快速但有效的示例,希望它现在可读性更好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-23
    • 1970-01-01
    相关资源
    最近更新 更多