web-dev-qa-db-fra.com

Arrêtez de manière fiable System.Threading.Timer?

Eh bien, j'ai beaucoup cherché une solution à ce problème. Je cherche un moyen simple et clair d'empêcher que la méthode de rappel d'un System.Threading.Timer soit invoquée une fois que je l'ai arrêtée. 

Je n'arrive pas à en trouver, et cela m’a occasionnellement amené à recourir au redoutable thread-thread.sleep-thread.abort combo shudders .

Peut-on le faire en utilisant un verrou? S'il vous plaît aidez-moi à trouver un bon moyen de le faire. Merci

49
JayPea

comme Conrad Frix a suggéré d'utiliser plutôt la classe System.Timers.Timer, comme:

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

Une solution plus simple consiste peut-être à définir la variable Timer pour qu'elle ne reprenne jamais; la méthode Timer.Change peut prendre les valeurs pour dueTime et period qui ordonnent au minuteur de ne jamais redémarrer:

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

Changer pour utiliser System.Timers.Timer pourrait être une "meilleure" solution, il y aura toujours des moments où ce ne sera pas pratique; simplement utiliser Timeout.Infinite devrait suffire.

97
Owen Blacker

Pour le System.Threading.Timer, vous pouvez procéder comme suit (cela protégera également la méthode de rappel du travail sur un minuteur supprimé - 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);
            }
        }
    }
}

Voici comment on peut l'utiliser:

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

Les MSDN Docs vous suggèrent d’utiliser la méthode Dispose(WaitHandle) pour arrêter le chronomètre et être informé de tout rappel des rappels.

7
Will A

Pour ce que cela vaut, nous utilisons un peu ce motif:

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

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

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

Pour moi, cela semble être la bonne façon de procéder: Il suffit d'appeler dispose lorsque vous avez terminé avec le minuteur. Cela arrêtera le chronomètre et empêchera les futurs appels programmés.

Voir exemple ci-dessous.

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

Cette réponse concerne System.Threading.Timer

J'ai lu beaucoup de bêtises sur la synchronisation de la suppression de System.Threading.Timer sur le net. C’est pourquoi je publie ce message dans le but de corriger quelque peu la situation. N'hésitez pas à me dénoncer ou à m'appeler si quelque chose que j'écris est faux ;-)

Les pièges

À mon avis, il y a ces pièges:

  • Timer.Dispose(WaitHandle) peut retourner false. Cela se produit au cas où cela aurait déjà été éliminé (j'ai dû regarder le code source). Dans ce cas, ne définira pas la WaitHandle - alors n'attendez pas!
  • ne gère pas un délai d'attente WaitHandle. Sérieusement - qu'attendez-vous si vous n'êtes pas intéressé par un délai d'attente?
  • Problème de simultanéité tel que mentionné ici sur msdn où une ObjectDisposedException peut se produire pendant la cession de (pas après).
  • Timer.Dispose(WaitHandle) ne fonctionne pas correctement avec -Slim waithandles, ou pas comme on pourrait s'y attendre. Par exemple, not ne fonctionne pas comme suit (il bloque pour toujours):
 using(var manualResetEventSlim = new ManualResetEventSlim)
 {
     timer.Dispose(manualResetEventSlim.WaitHandle);
     manualResetEventSlim.Wait();
 }

Solution

Eh bien, le titre est un peu "gras" je suppose, mais voici ma tentative de traiter le problème - un wrapper qui gère la double élimination, les délais d'attente et ObjectDisposedException. Cependant, il ne fournit pas toutes les méthodes de Timer - mais n'hésitez pas à les ajouter.

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;
            }
        }
    }
}
2
BatteryBackupUnit

Peut-être devriez-vous faire le contraire. Utilisez system.timers.timer, définissez AutoReset sur false et démarrez-le uniquement lorsque vous le souhaitez.

1
Conrad Frix

Vous ne pouvez pas garantir que votre code censé arrêter le minuteur s'exécutera avant l'appel de l'événement minuteur ..__ Par exemple, supposons que le moment 0 ait initialisé le chronomètre pour appeler l'événement lorsque le moment 5 arrive. Puis, à l'heure 3, vous avez décidé que vous n'aviez plus besoin de l'appel. Et appelé méthode que vous voulez écrire ici. Puis, alors que la méthode était JIT-ted, vient le moment 4 et le système d'exploitation décide que votre thread utilise sa tranche de temps et son commutateur. Et la minuterie invoquera l'événement peu importe comment vous essayez - votre code n'aura tout simplement pas la chance de s'exécuter dans le pire des cas.

C'est pourquoi il est plus prudent de fournir une certaine logique dans le gestionnaire d'événements. Peut-être que certains ManualResetEvent seront réinitialisés dès que vous n’auriez plus besoin d’invocation d’événement. Donc, vous supprimez le minuteur, puis définissez le ManualResetEvent. Et dans le gestionnaire d’événements timer, vous commencez par tester ManualResetEvent. S'il est en état de réinitialisation, revenez immédiatement. Ainsi, vous pouvez vous prémunir contre l'exécution non souhaitée de certains codes.

1
Ivan Danilov

Il existe un lien MSDN permettant d’atteindre le compteur d’arrêt correctement. Utilisez la méthode ControlThreadProc() avec l'événement HandleElapsed(object sender, ElapsedEventArgs e) synchronisé par la variable de classe statique syncPoint. Mettez en commentaire Thread.Sleep(testRunsFor); sur ControlThreadProc() si cela ne convient pas (probablement) . La clé est celle qui utilise une variable statique et une opération atomique comme Interlocked.CompareExchange sur des instructions conditionnelles.

Lien: Méthode Timer.Stop

0

Vous pouvez arrêter un minuteur en créant une classe comme celle-ci et en l'appelant, par exemple, avec votre méthode de rappel:

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

Instanciation du minuteur:

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

Puis à l'intérieur de la méthode de rappel:

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