web-dev-qa-db-de.com

Beenden Sie System.Threading.Timer zuverlässig?

Nun, ich habe viel nach einer Lösung dafür gesucht. Ich suche nach einer sauberen und einfachen Methode, um zu verhindern, dass die Callback-Methode eines System.Threading.Timer aufgerufen wird, nachdem ich sie angehalten habe. 

Ich kann scheinbar keine finden, und das hat mich gelegentlich dazu veranlasst, auf den gefürchteten Thread-Thread.sleep-thread.abort-Combo shudders zuzugreifen.

Kann es mit einem Schloss gemacht werden? Bitte helfen Sie mir dabei, einen guten Weg zu finden. Vielen Dank

49
JayPea

wie Conrad Frix schlug vor, Sie sollten stattdessen die System.Timers.Timer-Klasse verwenden, wie:

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

Eine einfachere Lösung könnte sein, die Variable Timer nie wiederaufzunehmen. Die Methode Timer.Change kann Werte für dueTime und period annehmen, die den Timer anweisen, niemals neu zu starten:

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

Während der Wechsel zu System.Timers.Timer eine "bessere" Lösung sein kann, wird es immer Zeiten geben, in denen dies nicht praktikabel ist. Es genügt, nur Timeout.Infinite zu verwenden.

97
Owen Blacker

Für den System.Threading.Timer kann man Folgendes tun (schützt auch die Callback-Methode vor der Bearbeitung eines bereitgestellten Timers - 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);
            }
        }
    }
}

So kann man es benutzen:

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
}
10
Rolf Kristensen

Die MSDN-Dokumente schlagen vor, dass Sie die Dispose(WaitHandle)-Methode verwenden, um den Timer zu stoppen + informiert zu werden, wenn keine Rückrufe mehr aufgerufen werden.

7
Will A

Für das, was es wert ist, verwenden wir dieses Muster ziemlich viel:

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

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

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

Diese Antwort bezieht sich auf System.Threading.Timer

Ich habe viel Unsinn darüber gelesen, wie die Entsorgung von System.Threading.Timer Über das gesamte Netz synchronisiert werden kann. Deshalb poste ich dies, um die Situation etwas zu verbessern. Sag mir Bescheid/ruf mich an, wenn etwas falsch ist ;-)

Fallstricke

Meiner Meinung nach gibt es diese Fallstricke:

  • Timer.Dispose(WaitHandle) kann false zurückgeben. Dies gilt für den Fall, dass es bereits entsorgt wurde (ich musste mir den Quellcode ansehen). In diesem Fall wird nicht setze den WaitHandle - also warte nicht darauf!
  • es wird keine WaitHandle Zeitüberschreitung behandelt. Im Ernst - worauf warten Sie, falls Sie nicht an einer Auszeit interessiert sind?
  • Nebenläufigkeitsproblem wie erwähnt hier auf msdn wobei ein ObjectDisposedException auftreten kann während (nicht nach) Entsorgung.
  • Timer.Dispose(WaitHandle) funktioniert nicht richtig mit -Slim waithandles oder nicht wie erwartet. Zum Beispiel funktioniert nicht (es blockiert für immer):
 using(var manualResetEventSlim = new ManualResetEventSlim)
 {
     timer.Dispose(manualResetEventSlim.WaitHandle);
     manualResetEventSlim.Wait();
 }

Lösung

Nun, der Titel ist ein bisschen "fett", aber unten ist mein Versuch, das Problem zu lösen - ein Wrapper, der Double-Disposal, Timeouts und ObjectDisposedException behandelt. Es werden jedoch nicht alle Methoden für Timer bereitgestellt. Sie können sie jedoch hinzufügen.

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

    public void Dispose()
    {
        using (var waitHandle = new ManualResetEvent(false))
        {
            // returns false on second dispose
            if (_timer.Dispose(waitHandle))
            {
                if (!waitHandle.WaitOne(_disposalTimeout))
                {
                    throw new TimeoutException(
                        "Timeout waiting for timer to stop. (...)");
                }
                _disposeEnded = true;
            }
        }
    }
}
3

Für mich scheint dies der richtige Weg zu sein: Rufen Sie einfach dispose auf, wenn Sie mit dem Timer fertig sind. Dadurch wird der Timer angehalten und zukünftige geplante Anrufe werden verhindert.

Siehe Beispiel unten.

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);
    }
}
3
user4908274

Vielleicht sollten Sie das Gegenteil tun. Verwenden Sie system.timers.timer, setzen Sie AutoReset auf false und starten Sie es nur dann, wenn Sie möchten

1
Conrad Frix

Sie können nicht garantieren, dass Ihr Code, der den Timer anhalten soll, vor dem Aufruf des Timerereignisses ausgeführt wird ..__ Angenommen, Sie haben den Timer zum Zeitpunkt 0 initialisiert, zu dem das Ereignis aufgerufen wird, wenn der Zeitpunkt 5 kommt. Dann haben Sie zum Zeitpunkt 3 entschieden, dass Sie den Anruf nicht mehr benötigen. Und die Methode, die Sie hier schreiben möchten. Dann, während die Methode JIT-ted war, kommt der Zeitpunkt 4 und das Betriebssystem entscheidet, dass der Thread seine Zeitscheibe erschöpft und umgeschaltet hat. Der Timer ruft das Ereignis auf, egal wie Sie es versuchen - Ihr Code hat im schlimmsten Fall keine Chance, ausgeführt zu werden.

Aus diesem Grund ist es sicherer, im Event-Handler etwas Logik bereitzustellen. Möglicherweise ein ManualResetEvent, das zurückgesetzt wird, sobald Sie den Ereignisaufruf nicht mehr benötigen. So entsorgen Sie den Timer und stellen dann das ManualResetEvent ein. Im Timer-Event-Handler testen Sie zuerst ManualResetEvent. Wenn es sich im Reset-Zustand befindet, kehren Sie einfach sofort zurück. So können Sie effektiv vor unerwünschter Ausführung von Code schützen.

1
Ivan Danilov

Es gibt eine MSDN-Verknüpfung, wie der Stopp-Timer korrekt erreicht werden kann. Verwenden Sie die ControlThreadProc()-Methode mit dem HandleElapsed(object sender, ElapsedEventArgs e)-Ereignis, das mit der syncPoint statischen Klassenvariablen synchronisiert ist. Kommentieren Sie Thread.Sleep(testRunsFor); für ControlThreadProc() aus, wenn es nicht geeignet ist (wahrscheinlich) . Der Schlüssel ist der, der statische Variablen verwendet und eine atomare Operation wie Interlocked.CompareExchange für bedingte Anweisungen.

Link: Timer.Stop-Methode

Sie können einen Timer stoppen, indem Sie eine Klasse wie folgt erstellen und sie beispielsweise von Ihrer Callback-Methode aus aufrufen:

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

Instantiierungstimer:

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

Dann in der Callback-Methode:

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