在其回调方法中停止计时器

18

我有一个 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指出,回调方法在来自线程池的单独线程中调用(每次计时器触发)。这意味着,如果我在方法中首先停止计时器,它仍然不一定能够阻止计时器在第一个实例有机会停止计时器之前运行另一个方法实例。

也许应该锁定计时器(或甚至是非可重入方法)?如何正确地防止计时器在执行其回调(和非可重入)方法期间触发?


这个问题可能会对你有帮助 https://dev59.com/uXNA5IYBdhLWcg3wC5JR - Kane
5个回答

47

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

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

+1 我从来没有想过TryEnter的用途;这非常有趣。 - Schmuli
这似乎是行得通的。两个或更多线程可能会进入该方法,但只有一个会实际工作。我还在 Monitor.TryEnter() 之后停止了计时器,以便在执行期间根本不触发它(没有必要触发它),以防执行时间远大于计时器的周期。在方法完成后,计时器将重新启动。 - Kornelije Petak
根据应用程序所有者的要求而定。在您的解决方案中,计时器将每10秒调用一次,因此如果不进入下一个尝试,则下一个尝试将为20秒。当我们停止/启动计时器时,处理程序的下一个调用将在完成处理程序之前的10秒后进行。 - Sebastian Xawery Wiśniowiecki

7
一些可能的解决方案:
  • 在另一个等待事件的线程委托中完成实际工作。定时器回调仅发出事件信号。工作线程不能重入,因为它是一个单独的线程,只有在响应事件时才执行其工作。定时器可重入,因为它的作用仅是发出事件信号(似乎有点绕路和浪费,但这样也可以工作)
  • 创建一个仅具有起始超时而没有周期性超时的定时器,使其仅触发一次。定时器回调将处理该定时器对象并在完成工作后创建一个新的仅触发一次的定时器。

您可能能够通过使用原始定时器对象的 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分支和异常)时,代码变得不太干净。但在性能方面似乎是一个很好的解决方案,因为没有使用同步机制。除此之外,如果有其他方法/线程/定时器尝试进入此方法,则此方法将无法工作。然后,当然,我们正在重新进入非可重入方法,这就是监视器更好的地方。无论如何,感谢您的解决方案和测试。这是一个不错的想法。 - Kornelije Petak
代码的复杂性并不是问题,就像使用互斥锁一样 - 只需在 try/finally 中包装代码或简单地调用另一个具有复杂性的例程,并使计时器回调例程成为一个简单的“调用然后重置计时器”。如果回调将被多个计时器使用,则此技术将无法正常工作,您需要一个真正的同步对象。我发现计时器回调被多个计时器使用的情况相当罕见 - 特别是当回调非常复杂时(这通常意味着计时器回调是为非常特定的目的而设计的)。 - Michael Burr
您可以使用System.Timers.Timer来实现相同的效果,我认为这样更容易。请参见https://dev59.com/iFrUa4cB1Zd3GeqPhz_U。 - goku_da_master

0

我曾经遇到过一个类似的情况,使用了System.Timers.Timer,其中Elapsed事件是从线程池中执行的,并且需要可重入。

我使用了以下方法来解决这个问题:

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事件触发之前不给予处理器时间给您的事件处理程序,那么两个不同的线程可能会并行执行该方法。 - Kornelije Petak
好主意!您可以始终将其与像jsw的锁定解决方案一起使用。 - ParmesanCodice

0
    //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
    // 

我建议使用代码格式化工具或其他东西来整理那段代码,包括空格、格式等。它可能很棒,但第一印象很重要,我的第一印象是我不想使用看起来那样的代码。 - ProfK
这只是一个快速的示例,希望现在更易读了。 - Biniam Eyakem

0

我使用提供原子操作的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;
        }

    }

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