75 votes

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

J'ai beaucoup cherché une solution à ce problème. Je cherche un moyen propre et simple d'empêcher la méthode de rappel d'un System.Threading.Timer d'être invoquée après que je l'ai arrêté.

Je n'arrive pas à en trouver, ce qui m'a conduit, à l'occasion, à recourir à la redoutable combinaison thread-thread.sleep-thread.abort.

Peut-on le faire en utilisant la serrure ?

4voto

BatteryBackupUnit Points 2509

Cette réponse concerne System.Threading.Timer.

J'ai lu beaucoup d'absurdités sur la façon de synchroniser l'élimination de System.Threading.Timer partout sur le net. C'est pourquoi je poste ce texte afin de tenter de rectifier quelque peu la situation. N'hésitez pas à me réprimander ou à m'interpeller si j'écris quelque chose de faux ;-)

Pièges

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

  • Timer.Dispose(WaitHandle) peut retourner false. Il le fait dans le cas où il a déjà été éliminé (j'ai dû regarder le code source). Dans ce cas, il ne le fera pas fixer le WaitHandle - alors n'attendez pas !
  • ne pas gérer un WaitHandle temps mort. Sérieusement, qu'attendez-vous si vous n'êtes pas intéressé par un délai d'attente ?
  • Problème de simultanéité comme mentionné ici sur msdn où un ObjectDisposedException peut se produire pendant (pas après) l'élimination.
  • Timer.Dispose(WaitHandle) ne fonctionne pas correctement avec - Slim waithandles, ou pas comme on pourrait s'y attendre. Par exemple, le texte suivant fait no travail (ça bloque pour toujours) :

    using(var manualResetEventSlim = new ManualResetEventSlim) { timer.Dispose(manualResetEventSlim.WaitHandle); manualResetEventSlim.Wait(); }

Solution

Le titre est un peu "audacieux", je suppose, mais voici ma tentative de résoudre le problème : un wrapper qui gère la double élimination, les délais d'attente et les problèmes de sécurité. ObjectDisposedException . Il ne fournit pas toutes les méthodes sur 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;
            }
        }
    }
}

3voto

Ivan Danilov Points 5719

Vous ne pouvez pas garantir que votre code censé arrêter le timer s'exécutera avant l'invocation de l'événement timer. Par exemple, supposons qu'à l'instant 0, vous ayez initialisé le timer pour appeler l'événement à l'instant 5. Puis, à l'instant 3, vous décidez que vous n'avez plus besoin de cet appel. Et vous avez appelé la méthode que vous voulez écrire ici. Puis, pendant que la méthode était JIT-ted, le moment 4 arrive et le système d'exploitation décide que votre thread a épuisé sa tranche de temps et change. Et le timer invoquera l'événement, peu importe comment vous essayez - votre code n'aura tout simplement aucune chance de s'exécuter dans le pire des cas.

C'est pourquoi il est plus sûr de fournir une certaine logique dans le gestionnaire d'événements. Peut-être un ManualResetEvent qui sera réinitialisé dès que l'invocation de l'événement ne sera plus nécessaire. Donc, vous déposez le timer, puis vous définissez le ManualResetEvent. Et dans le gestionnaire d'événement de la minuterie, la première chose que vous faites est de tester le ManualResetEvent. S'il est dans l'état de réinitialisation, il suffit de revenir immédiatement. Ainsi, vous pouvez vous prémunir efficacement contre l'exécution indésirable de certains codes.

3voto

user4908274 Points 29

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

Voir l'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);
    }
}

2voto

Conrad Frix Points 34272

Vous devriez peut-être faire le contraire. Utilisez system.timers.timer, définissez l'AutoReset sur false et ne le démarrez que lorsque vous le souhaitez.

2voto

Stonetip Points 514

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

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

Instanciation de la minuterie :

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

Puis dans la méthode de rappel :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X