【问题标题】:Reliably stop System.Threading.Timer?可靠地停止 System.Threading.Timer?
【发布时间】:2011-06-16 22:56:20
【问题描述】:

好吧,我已经搜索了很多解决方案。我正在寻找一种干净简单的方法来防止 System.Threading.Timer 的回调方法在我停止后被调用。

我似乎找不到任何东西,这导致我偶尔求助于可怕的 thread-thread.sleep-thread.abort 组合。

可以用锁来完成吗?

【问题讨论】:

  • after 不是你真正的问题。停止它同时回调运行是困难的情况。这是很有可能的。使用锁。
  • 嗯,虽然问题不大,但还是应该检查和处理。否则回调可能会尝试使用一些已经被释放的资源或假设它们从未被再次调用等。我已经看到很多次由于回调中的错误假设导致应用程序退出时出现奇怪的错误。

标签: c# multithreading timer


【解决方案1】:

一个更简单的解决方案可能是将Timer 设置为永不恢复;方法Timer.Change 可以采用dueTimeperiod 的值,指示计时器永不重新启动:

this.Timer.Change(Timeout.Infinite, Timeout.Infinite);

虽然改用System.Timers.Timer 可能是一个“更好”的解决方案,但总会有不切实际的时候;只需使用Timeout.Infinite 就足够了。

【讨论】:

  • 这是最好的答案。海报和读者很可能出于充分的理由(比如我)使用 System.Threading.Timers。这个方法只是停止计时器,这正是我所需要的。
【解决方案2】:

like Conrad Frix 建议您应该改用System.Timers.Timer 类,例如:

private System.Timers.Timer _timer = new System.Timers.Timer();
private volatile bool _requestStop = false;

public constructor()
{
    _timer.Interval = 100;
    _timer.Elapsed += OnTimerElapsed;
    _timer.AutoReset = false;
    _timer.Start();
}

private void OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    // do work....
    if (!_requestStop)
    {
        _timer.Start();//restart the timer
    }
}

private void Stop()
{
    _requestStop = true;
    _timer.Stop();
}

private void Start()
{
    _requestStop = false;
    _timer.Start();
}

【讨论】:

  • 谢谢。我试过这个,它似乎正在工作。我不禁觉得某处存在竞争条件,但实际上它并没有失败。其他人可以对此发表评论吗?
  • 把 AutoReset = false 然后在 Elapsed-method 中做 _timer.Stop() 的原因是什么? AutoReset=true 不会完成同样的事情吗?
  • 我对这个答案感到困惑的原因是它似乎没有解决问题。这里的 Stop() 调用者仍然不能确定在 Stop() 调用之后没有执行“做工作”部分。我相信这是最初的问题/问题。
  • @Kyberias:将 AutoReset 设置为 false 并不能解决任何问题,但是我错误地将其留在那里..
  • 您对 Stop() 的实现不能保证任何活动线程都离开了 OnTimerElapsed()。因此,如果之后释放/停止 OnTimerElapsed() 使用的其他对象,则会发生竞争条件。
【解决方案3】:

MSDN Docs 建议您使用Dispose(WaitHandle) 方法来停止计时器+在不再调用回调时得到通知。

【讨论】:

    【解决方案4】:

    对于 System.Threading.Timer,可以执行以下操作(还将保护回调方法不处理已处置的计时器 - ObjectDisposedException):

    class TimerHelper : IDisposable
    {
        private System.Threading.Timer _timer;
        private readonly object _threadLock = new object();
    
        public event Action<Timer,object> TimerEvent;
    
        public void Start(TimeSpan timerInterval, bool triggerAtStart = false,
            object state = null)
        {
            Stop();
            _timer = new System.Threading.Timer(Timer_Elapsed, state,
                System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
    
            if (triggerAtStart)
            {
                _timer.Change(TimeSpan.FromTicks(0), timerInterval);
            }
            else
            {
                _timer.Change(timerInterval, timerInterval);
            }
        }
    
        public void Stop(TimeSpan timeout = TimeSpan.FromMinutes(2))
        {
            // Wait for timer queue to be emptied, before we continue
            // (Timer threads should have left the callback method given)
            // - http://woowaabob.blogspot.dk/2010/05/properly-disposing-systemthreadingtimer.html
            // - http://blogs.msdn.com/b/danielvl/archive/2011/02/18/disposing-system-threading-timer.aspx
            lock (_threadLock)
            {
                if (_timer != null)
                {
                    ManualResetEvent waitHandle = new ManualResetEvent(false)
                    if (_timer.Dispose(waitHandle))
                    {
                       // Timer has not been disposed by someone else
                       if (!waitHandle.WaitOne(timeout))
                          throw new TimeoutException("Timeout waiting for timer to stop");
                    }
                    waitHandle.Close();   // Only close if Dispose has completed succesful
                    _timer = null;
                }
            }
        }
    
        public void Dispose()
        {
            Stop();
            TimerEvent = null;
        }
    
        void Timer_Elapsed(object state)
        {
            // Ensure that we don't have multiple timers active at the same time
            // - Also prevents ObjectDisposedException when using Timer-object
            //   inside this method
            // - Maybe consider to use _timer.Change(interval, Timeout.Infinite)
            //   (AutoReset = false)
            if (Monitor.TryEnter(_threadLock))
            {
                try
                {
                    if (_timer==null)
                        return;
    
                    Action<Timer, object> timerEvent = TimerEvent;
                    if (timerEvent != null)
                    {
                        timerEvent(_timer, state);
                    }
                }
                finally
                {
                    Monitor.Exit(_threadLock);
                }
            }
        }
    }
    

    这是如何使用它的:

    void StartTimer()
    {
        TimerHelper _timerHelper = new TimerHelper();
        _timerHelper.TimerEvent += (timer,state) => Timer_Elapsed();
        _timerHelper.Start(TimeSpan.FromSeconds(5));
        System.Threading.Sleep(TimeSpan.FromSeconds(12));
        _timerHelper.Stop();
    }
    
    void Timer_Elapsed()
    {
       // Do what you want to do
    }
    

    【讨论】:

    • 这似乎不能可靠地工作。即使没有更多的回调正在运行,我也经常会超时。但是当然,您的代码不会检查超时,所以...
    • @BatteryBackupUnit 如果计时器执行的任务超过 2 分钟,那么是的,此实现将失败。我可能会在状态对象中添加一个取消令牌并在您的 TimerEvent-subscription-method 中监视它(或将超时更改为无限)
    【解决方案5】:

    为了它的价值,我们经常使用这种模式:

    // set up timer
    Timer timer = new Timer(...);
    ...
    
    // stop timer
    timer.Dispose();
    timer = null;
    ...
    
    // timer callback
    {
      if (timer != null)
      {
        ..
      }
    }
    

    【讨论】:

    • 这不是线程安全的,因为 (1) timer = null 可以从线程更新,并且回调线程仍然可以看到它的旧值。 (2) 如果你使用定时器内部的定时器实例,在“if (timer!= null)”检查后回调;它可能为空,因为另一个线程调用了 stop 方法并使其为空。我真的不推荐这种模式,除非你为线程安全做一些事情,比如锁......
    • @JalalAldeenSaa'd:如果 Timer 变量被标记为“volatile”可以吗?
    • @RenniePet: 不,即使它是 volatile 仍然可以发生比赛,'volatile' 不会禁止更新计时器变量,即它不是一个锁定语句
    【解决方案6】:

    这个答案与 System.Threading.Timer 相关

    我已经阅读了很多关于如何在网上同步处理System.Threading.Timer 的废话。所以这就是为什么我发布这个试图在某种程度上纠正这种情况。如果我写的东西有问题,请随时告诉我/打电话给我;-)

    陷阱

    在我看来存在以下陷阱:

    • Timer.Dispose(WaitHandle) 可以返回 false。如果它已经被处理(我必须查看源代码),它会这样做。在这种情况下,它不会设置WaitHandle - 所以不要等待它!
    • 未处理WaitHandle 超时。说真的 - 如果您对超时不感兴趣,您还在等什么?
    • here on msdn 提到的并发问题,其中ObjectDisposedException 可能发生在处理期间(而不是处理之后)。
    • Timer.Dispose(WaitHandle) 不能与 -Slim 等待句柄一起正常工作,或者与预期不同。例如,以下操作起作用(它永远阻塞):
     using(var manualResetEventSlim = new ManualResetEventSlim)
     {
         timer.Dispose(manualResetEventSlim.WaitHandle);
         manualResetEventSlim.Wait();
     }
    

    解决方案

    我猜这个标题有点“粗体”,但下面是我处理这个问题的尝试——一个处理双重处理、超时和ObjectDisposedException的包装器。虽然它没有提供 Timer 上的所有方法 - 但请随意添加它们。

    internal class Timer
    {
        private readonly TimeSpan _disposalTimeout;
        
        private readonly System.Threading.Timer _timer;
    
        private bool _disposeEnded;
    
        public Timer(TimeSpan disposalTimeout)
        {
            _disposalTimeout = disposalTimeout;
            _timer = new System.Threading.Timer(HandleTimerElapsed);
        }
    
        public event Action Elapsed;
    
        public void TriggerOnceIn(TimeSpan time)
        {
            try
            {
                _timer.Change(time, Timeout.InfiniteTimeSpan);
            }
            catch (ObjectDisposedException)
            {
                // race condition with Dispose can cause trigger to be called when underlying
                // timer is being disposed - and a change will fail in this case.
                // see 
                // https://msdn.microsoft.com/en-us/library/b97tkt95(v=vs.110).aspx#Anchor_2
                if (_disposeEnded)
                {
                    // we still want to throw the exception in case someone really tries
                    // to change the timer after disposal has finished
                    // of course there's a slight race condition here where we might not
                    // throw even though disposal is already done.
                    // since the offending code would most likely already be "failing"
                    // unreliably i personally can live with increasing the
                    // "unreliable failure" time-window slightly
                    throw;
                }
            }
        }
    
        private void HandleTimerElapsed(object state)
        {
            Elapsed?.Invoke();
        }
    
        public void Dispose()
        {
            var waitHandle = new ManualResetEvent(false));
    
            // returns false on second dispose
            if (_timer.Dispose(waitHandle))
            {
                if (waitHandle.WaitOne(_disposalTimeout))
                {
                    _disposeEnded = true;
                    waitHandle.Dispose();
                }
                else
                {
                    // don't dispose the wait handle, because the timer might still use it.
                    // Disposing it might cause an ObjectDisposedException on 
                    // the timer thread - whereas not disposing it will 
                    // result in the GC cleaning up the resources later
                    throw new TimeoutException(
                        "Timeout waiting for timer to stop. (...)");
                }
            }
        }
    }
    

    【讨论】:

    • 考虑在 Dispose() 中删除 ManualResetEvent 周围的 using 语句。如果 waitHandle.WaitOne() 失败,它会抛出异常并释放事件句柄,当计时器最终完成时,它会抛出 ObjectDisposedException。
    • @RolfKristensen 谢谢,但我不太明白。删除using 有什么好处?假设WaitOne(x)ArgumentOutOfRangeException 失败 - 会发生什么?我不认为ManualResetEvent 在这种情况下会被处理掉。根据文档,ManualResetEvent.Dispose() 在任何情况下都不会引发异常,因此using 隐藏另一个异常也不应该有问题。
    • 你将 waitHandle 交给 timer.Dispose()。这意味着计时器将在完成时调用 waitHandle。当您抛出 TimeoutException 时,using 语句将关闭 waitHandle。当计时器线程最终调用 waitHandle 时,它​​会遇到 ObjectDisposedException。
    • @RolfKristensen 我明白了。它可能会导致未处理的异常-> 立即关闭进程(我还没有测试过;也许 Timer 本身也在吞噬异常......)。在这种情况下,我同意仅在 WaitOne 返回 true 时进行处理。
    【解决方案7】:

    您不能保证应该停止计时器的代码会在计时器事件调用之前执行。 例如,假设在时间点 0,您初始化计时器以在时间点 5 到来时调用事件。然后在时间点 3,您决定不再需要该呼叫。并调用你想在这里写的方法。然后,当方法是 JIT-ted 时,时间点 4 和操作系统决定你的线程耗尽它的时间片并切换。无论您如何尝试,计时器都会调用该事件 - 您的代码在最坏的情况下将没有机会运行。

    这就是为什么在事件处理程序中提供一些逻辑更安全的原因。也许一些 ManualResetEvent 将在您不再需要事件调用时立即重置。所以你 Dispose 定时器,然后设置 ManualResetEvent。在计时器事件处理程序中,您首先要做的是测试 ManualResetEvent。如果它处于重置状态 - 立即返回。因此,您可以有效地防止某些代码的意外执行。

    【讨论】:

    • 为什么不提出一个独立的解决方案?
    • 存在独立的解决方案,它被称为Timer。它只需要正确使用。
    • 我的建议是,既然有可能错误地使用 Timer,为什么不创建一个在这方面不能错误使用的可重用类。该类可以包含复杂性,因此您不必担心。
    • 其实我想不出几乎任何同步仪器都不能用错。除了计时器,我不会尝试提出解决方案,因为作为通用工具,它就像我想象的那样简单。为什么要让事情变得更复杂和容易出错?我认为它不会简化任何事情。
    【解决方案8】:

    对我来说,这似乎是正确的方法: 完成计时器后,只需致电dispose。这将停止计时器并阻止未来的预定呼叫。

    请参阅下面的示例。

    class Program
    {
        static void Main(string[] args)
        {
            WriteOneEverySecond w = new WriteOneEverySecond();
            w.ScheduleInBackground();
            Console.ReadKey();
            w.StopTimer();
            Console.ReadKey();
        }
    }
    
    class WriteOneEverySecond
    {
        private Timer myTimer;
    
        public void StopTimer()
        {
            myTimer.Dispose();
            myTimer = null;
        }
    
        public void ScheduleInBackground()
        {
            myTimer = new Timer(RunJob, null, 1000, 1000);
        }
    
        public void RunJob(object state)
        {
            Console.WriteLine("Timer Fired at: " + DateTime.Now);
        }
    }
    

    【讨论】:

      【解决方案9】:

      也许你应该做相反的事情。使用 system.timers.timer,将 AutoReset 设置为 false 并仅在需要时启动它

      【讨论】:

        【解决方案10】:

        您可以通过创建这样的类并从例如您的回调方法调用它来停止计时器:

        public class InvalidWaitHandle : WaitHandle
        {
            public IntPtr Handle
            {
                get { return InvalidHandle; }
                set { throw new InvalidOperationException(); }
            }
        }
        

        实例化计时器:

        _t = new Timer(DisplayTimerCallback, TBlockTimerDisplay, 0, 1000);
        

        然后内部回调方法:

        if (_secondsElapsed > 80)
        {
            _t.Dispose(new InvalidWaitHandle());
        }
        

        【讨论】:

          【解决方案11】:

          有一个 MSDN 链接如何正确实现停止计时器。使用ControlThreadProc() 方法和HandleElapsed(object sender, ElapsedEventArgs e) 事件,由syncPoint 静态类变量同步。如果不合适(可能),请在 ControlThreadProc() 上注释掉 Thread.Sleep(testRunsFor);。 关键是在条件语句上使用静态变量和像Interlocked.CompareExchange 这样的原子操作。

          链接: Timer.Stop Method

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-02-09
            • 1970-01-01
            • 2012-01-28
            相关资源
            最近更新 更多