可靠地停止System.Threading.Timer?

72

我为此搜寻了许多解决方案。我正在寻找一种简单干净的方法来防止在停止后调用System.Threading.Timer的回调方法。

我似乎找不到任何方法,这有时会导致我使用可怕的线程-线程.sleep-线程.abort组合。

是否可以使用锁定(lock)来实现?


2
after 不是你真正的问题。在回调运行时停止它是困难的情况。这是完全可能的。使用锁。 - Hans Passant
1
虽然这不是一个大问题,但仍应该进行检查和处理。否则回调函数可能会尝试使用已经释放的资源或假设它们从未被再次调用等。我曾多次看到由于回调函数中的错误假设而导致应用程序退出时出现奇怪的错误。 - Ivan Danilov
11个回答

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

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


15
这是最佳答案。发帖者和读者可能有很好的理由在使用 System.Threading.Timers(就像我一样)。这个方法只是停止了计时器,这正是我所需要的。 - Steve Hibbert
1
这是一个奇怪的删除底层计时器的方法。实际上,该值只是-1,它被转换为uint,基本上是uint.MaxValue(尽管他们没有明确使用它,而是使用了(uint)-1)。Timeout类说:“用于需要超时(Object.Wait、Thread.Sleep等)的方法的常量,表示不应发生超时。”计时器类根据此值显式禁用计时器。 - Dave Cousineau

44

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();
}

谢谢。我尝试了一下,似乎可以工作。但我不禁感到其中可能存在竞态条件,但实际上它并没有失败。有其他人能否对此发表评论? - JayPea
7
我对这个答案感到困惑的原因是它似乎没有解决问题。在此调用Stop()的调用者仍然无法确定在Stop()调用之后是否执行了“做工作”的部分。我相信这就是最初的问题/问题所在。 - Kyberias
1
你的Stop()实现并不能保证所有活动线程都已经离开OnTimerElapsed(),所以如果之后释放/停止其他OnTimerElapsed()正在使用的对象,就会发生竞争条件。 - Rolf Kristensen
@Rolf:不,它不会通过调用“Stop()”来终止活动线程,这段代码的目的不是确保没有正在运行并通过“OnTimerElapsed”中的第一个“if(!_requestStop)”的活动线程,仅此而已。该代码不用于取消“OnTimerElapsed”方法内部正在运行的代码,如果您想要这样做,您必须在“OnTimerElapced”内使用另一种方法,例如“CancellationTaken”等来处理此问题。 - Jalal Said
微软更喜欢使用System.Threading.Timer而不是System.Timers.Timer。 - Michael Freidgeim
显示剩余2条评论

14

MSDN文档建议使用Dispose(WaitHandle)方法来停止定时器,并在回调不再被调用时得到通知。


12

对于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 订阅方法中监视它(或将超时更改为无限)。 - Rolf Kristensen

6

就我们而言,我们经常使用这种模式:

// set up timer
Timer timer = new Timer(...);
...

// stop timer
timer.Dispose();
timer = null;
...

// timer callback
{
  if (timer != null)
  {
    ..
  }
}

11
由于(1)计时器 = null 可能会被从某个线程更新,但回调线程仍然可以看到它的旧值。(2)如果你在定时器回调中使用定时器实例,在“if (timer!= null)”检查之后它可能为空,因为另一个线程调用了停止方法并将其设置为null。我真的不建议使用这种模式,除非你为线程安全做一些像锁定这样的事情... - Jalal Said
1
@JalalAldeenSaa'd:如果计时器变量标记为“volatile”,这样可以吗? - RenniePet
@RenniePet:不,即使是volatile,竞争仍然可能发生,“volatile”并不禁止更新计时器变量,即它不是一个锁语句。 - Jalal Said

4

本答案与System.Threading.Timer有关

我在网上读了很多关于如何同步处理 System.Threading.Timer 销毁的无聊文章。这就是为什么我发布这篇文章,以尝试在某种程度上纠正这种情况。如果我所写的内容有误,请随时指出并批评我 ;-)

陷阱

我认为存在以下陷阱:

  • Timer.Dispose(WaitHandle) 可能返回 false。如果已经被销毁,则会这样做(我不得不查看源代码)。在这种情况下,它 不会 设置 WaitHandle - 因此不要等待它!
  • 未处理 WaitHandle 超时。说真的-如果您对超时不感兴趣,那么您在等待什么?
  • 并发问题,如 此处 msdn 上提到的,其中可能在销毁期间(而不是之后)发生 ObjectDisposedException 异常。
  • Timer.Dispose(WaitHandle) 与-Slim waithandles 不兼容,或者不像人们期望的那样工作。例如,以下代码 不会 正常工作(它永远阻塞):
 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异常。 - Rolf Kristensen
@RolfKristensen 谢谢,但我不太明白。删除 using 的好处是什么?假设 WaitOne(x) 失败并抛出 ArgumentOutOfRangeException,会发生什么?我认为在这种情况下 ManualResetEvent 不会被处理。 根据文档,ManualResetEvent.Dispose() 在任何情况下都不会抛出异常,因此使用 using 隐藏另一个异常也不应该有问题。 - BatteryBackupUnit
1
你将waitHandle传递给timer.Dispose()。这意味着当定时器完成时,定时器将调用waitHandle。当你抛出TimeoutException异常时,using语句将关闭waitHandle。当定时器线程最终调用waitHandle时,它将遇到ObjectDisposedException异常。 - Rolf Kristensen
@RolfKristensen 我明白了。这可能会导致未处理的异常 -> 进程立即关闭(我还没有测试过;也许计时器本身也会吞噬异常...)。在这种情况下,我同意只有在 WaitOne 返回 true 时才进行处理会更有益。 - BatteryBackupUnit
@BatteryBackupUnit 对不起我的无知,但我不清楚何时/如何使用TriggerOnceIn()。它的目的是在事件运行时停止计时器吗? - Mike Bruno
1
@MikeBruno 上面的Timer包装类示例不是一个重复定时器,而是每次调用TriggerOnceIn时,Timer.Elapsed事件将在您传递的TimeSpan time之后被触发。 如果在Timer.Elapsed事件之前多次调用TriggerOnceIn,则Timer.Elapsed事件应该仅发生一次(在指定的最晚时间)。请注意,由于并发性质,这不能保证。此外,Timer.Dispose()应该取消Elapsed的发生,但当然它可能会与同时发生。 - BatteryBackupUnit

3
您无法保证您的代码能在计时器事件被调用之前停止计时器。例如,假设在时间点0上,您初始化了计时器以在时间点5调用事件。然后在时间点3上,您决定不再需要该调用。并调用您想要编写的方法。然后当方法正在JIT-ting时,时间点4已经到来,操作系统决定您的线程耗尽其时间片并切换。而计时器将无论如何调用事件 - 在最坏的情况下,您的代码根本没有机会运行。

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


为什么不提出一个自包含的解决方案呢? - agent-j
1
存在一个自包含的解决方案,它被称为计时器。只需要正确使用它即可。 - Ivan Danilov
我建议既然可能会错误地使用计时器,为什么不创建一个可重复使用的类,在这方面无法被错误使用。该类可以包含复杂性,因此您不必担心它。 - agent-j
2
事实上,我几乎想不到任何同步工具完全不能被错误使用。除了计时器之外,我并不试图提出解决方案,因为作为一种通用工具,它就像我能想象的那样简单。为什么要让某些东西变得更加复杂和容易出错呢?我认为这并不会简化任何事情。 - Ivan Danilov

3
对于我来说,这似乎是正确的方法: 当你使用完计时器后,只需调用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);
    }
}

2
也许你可以反其道而行之。使用system.timers.timer,将AutoReset设置为false,并在需要时才启动它。

2
您可以通过创建一个类,并从您的回调方法中调用它来停止计时器,例如:
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());
}

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