System.Threading.Timer en C #, il semble ne pas fonctionner. Il tourne très vite toutes les 3 secondes

112

J'ai un objet minuterie. Je veux qu'il soit exécuté toutes les minutes. Plus précisément, il doit exécuter une OnCallBackméthode et devient inactif lorsqu'une OnCallBackméthode est en cours d'exécution. Une fois qu'une OnCallBackméthode se termine, elle (a OnCallBack) redémarre une minuterie.

Voici ce que j'ai en ce moment:

private static Timer timer;

private static void Main()
{
    timer = new Timer(_ => OnCallBack(), null, 0, 1000 * 10); //every 10 seconds
    Console.ReadLine();
}

private static void OnCallBack()
{
    timer.Change(Timeout.Infinite, Timeout.Infinite); //stops the timer
    Thread.Sleep(3000); //doing some long operation
    timer.Change(0, 1000 * 10);  //restarts the timer
}

Cependant, cela semble ne pas fonctionner. Il fonctionne très vite toutes les 3 secondes. Même si augmenter une période (1000 * 10). On dirait que ça ferme les yeux sur1000 * 10

Qu'ai-je fait de mal?

Alan Coromano
la source
12
From Timer.Change: "Si dueTime vaut zéro (0), la méthode de rappel est appelée immédiatement.". On dirait que c'est nul pour moi.
Damien_The_Unbeliever
2
Oui, mais alors quoi? il y a aussi une période.
Alan Coromano
10
Alors que faire s'il y a aussi une période? La phrase citée ne fait aucune déclaration sur la valeur de la période. Il dit simplement "si cette valeur est nulle, je vais appeler le rappel immédiatement".
Damien_The_Unbeliever
3
Fait intéressant, si vous définissez à la fois dueTime et period sur 0, le minuteur fonctionnera toutes les secondes et démarrera immédiatement.
Kelvin

Réponses:

230

Ce n'est pas l'utilisation correcte de System.Threading.Timer. Lorsque vous instanciez le minuteur, vous devez presque toujours effectuer les opérations suivantes:

_timer = new Timer( Callback, null, TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );

Cela demandera au chronomètre de ne cocher qu'une seule fois lorsque l'intervalle est écoulé. Ensuite, dans votre fonction de rappel, vous modifiez la minuterie une fois le travail terminé, pas avant. Exemple:

private void Callback( Object state )
{
    // Long running operation
   _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );
}

Il n'y a donc pas besoin de mécanismes de verrouillage car il n'y a pas de concurrence. La minuterie déclenchera le prochain rappel après que l'intervalle suivant se soit écoulé + le temps de l'opération longue.

Si vous devez exécuter votre minuterie à exactement N millisecondes, je vous suggère de mesurer le temps de l'opération de longue durée à l'aide du chronomètre, puis d'appeler la méthode Change de manière appropriée:

private void Callback( Object state )
{
   Stopwatch watch = new Stopwatch();

   watch.Start();
   // Long running operation

   _timer.Change( Math.Max( 0, TIME_INTERVAL_IN_MILLISECONDS - watch.ElapsedMilliseconds ), Timeout.Infinite );
}

Je fortement encourage tous ceux qui font .NET et utilise le CLR qui n'a pas lu le livre de Jeffrey Richter - CLR via C # , lire est le plus tôt possible. Les minuteries et les pools de threads y sont expliqués en détail.

Ivan Zlatanov
la source
6
Je ne suis pas d'accord avec ça private void Callback( Object state ) { // Long running operation _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite ); }. Callbackpeut être appelé à nouveau avant qu'une opération ne soit terminée.
Alan Coromano
2
Ce que je voulais dire, c'est que cela Long running operationpourrait prendre beaucoup plus de temps alors TIME_INTERVAL_IN_MILLISECONDS. Que se passerait-il alors?
Alan Coromano
32
Le rappel ne sera plus appelé, c'est le point. C'est pourquoi nous passons Timeout.Infinite comme deuxième paramètre. Cela signifie essentiellement ne pas cocher à nouveau pour la minuterie. Puis reprogrammez pour cocher une fois que nous avons terminé l'opération.
Ivan Zlatanov
Débutant dans le filetage ici - pensez-vous que cela soit possible avec un ThreadPool, si vous passez le chronomètre? Je pense à un scénario dans lequel un nouveau thread est généré pour faire un travail à un certain intervalle - puis relégué dans le pool de threads une fois terminé.
jedd.ahyoung
2
Le System.Threading.Timer est un minuteur de pool de threads, qui exécute ses rappels sur le pool de threads, pas un thread dédié. Une fois que le minuteur a terminé la routine de rappel, le thread qui a exécuté le rappel retourne dans le pool.
Ivan Zlatanov
14

Il n'est pas nécessaire d'arrêter la minuterie, voir la belle solution de ce post :

"Vous pouvez laisser le chronomètre continuer à déclencher la méthode de rappel, mais envelopper votre code non réentrant dans un Monitor.TryEnter / Exit. Inutile d'arrêter / redémarrer le chronomètre dans ce cas; les appels qui se chevauchent n'acquièrent pas le verrou et ne reviennent pas immédiatement."

private void CreatorLoop(object state) 
 {
   if (Monitor.TryEnter(lockObject))
   {
     try
     {
       // Work here
     }
     finally
     {
       Monitor.Exit(lockObject);
     }
   }
 }
Ivan Leonenko
la source
ce n'est pas mon cas. J'ai besoin d'arrêter la minuterie exactement.
Alan Coromano
Essayez-vous d'empêcher la saisie de rappel plus d'une fois? Si non, qu'essayez-vous de réaliser?
Ivan Leonenko
1. Empêchez d'entrer pour le rappel plus d'une fois. 2. Évitez d'exécuter trop de fois.
Alan Coromano
C'est exactement ce qu'il fait. # 2 n'est pas beaucoup de surcharge tant qu'il retourne juste après l'instruction if si l'objet est verrouillé, surtout si vous avez un si grand intervalle.
Ivan Leonenko
1
Cela ne garantit pas que le code soit appelé au moins <intervalle> après la dernière exécution (un nouveau tick du minuteur pourrait être déclenché une microseconde après qu'un tick précédent ait libéré le verrou). Cela dépend s'il s'agit d'une exigence stricte ou non (pas tout à fait clair d'après la description du problème).
Marco Mp
9

L'utilisation est-elle System.Threading.Timerobligatoire?

Sinon, System.Timers.Timera des méthodes pratiques Start()et Stop()(et une AutoResetpropriété que vous pouvez définir sur false, de sorte que le Stop()n'est pas nécessaire et que vous appelez simplement Start()après l'exécution).

Marco Mp
la source
3
Oui, mais cela pourrait être une vraie exigence, ou il s'est simplement produit que la minuterie ait été choisie parce que c'est la plus utilisée. Malheureusement .NET a des tonnes d'objets de minuterie, qui se chevauchent à 90% mais qui sont toujours (parfois subtilement) différents. Bien entendu, si c'est une exigence, cette solution ne s'applique pas du tout.
Marco Mp
2
Selon la documentation : la classe Systems.Timer est disponible dans le .NET Framework uniquement. Il n'est pas inclus dans la bibliothèque standard .NET et n'est pas disponible sur d'autres plates-formes, telles que .NET Core ou la plate-forme Windows universelle. Sur ces plates-formes, ainsi que pour la portabilité sur toutes les plates-formes .NET, vous devez utiliser la classe System.Threading.Timer à la place.
NotAgain dit réintégrer Monica
3

Je ferais juste:

private static Timer timer;
 private static void Main()
 {
   timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
   Console.ReadLine();
 }

  private static void OnCallBack()
  {
    timer.Dispose();
    Thread.Sleep(3000); //doing some long operation
    timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
  }

Et ignorez le paramètre de période, car vous essayez de contrôler la périodicité vous-même.


Votre code d'origine s'exécute aussi vite que possible, car vous continuez à spécifier 0le dueTimeparamètre. De Timer.Change:

Si dueTime vaut zéro (0), la méthode de rappel est appelée immédiatement.

Damien_The_Unbeliever
la source
2
Est-il nécessaire de disposer de la minuterie? Pourquoi n'utilisez-vous pas la Change()méthode?
Alan Coromano
21
Jeter la minuterie à chaque fois est absolument inutile et incorrect.
Ivan Zlatanov
0
 var span = TimeSpan.FromMinutes(2);
 var t = Task.Factory.StartNew(async delegate / () =>
   {
        this.SomeAsync();
        await Task.Delay(span, source.Token);
  }, source.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

source.Cancel(true/or not);

// or use ThreadPool(whit defaul options thread) like this
Task.Start(()=>{...}), source.Token)

si vous aimez utiliser un fil de boucle à l'intérieur ...

public async void RunForestRun(CancellationToken token)
{
  var t = await Task.Factory.StartNew(async delegate
   {
       while (true)
       {
           await Task.Delay(TimeSpan.FromSeconds(1), token)
                 .ContinueWith(task => { Console.WriteLine("End delay"); });
           this.PrintConsole(1);
        }
    }, token) // drop thread options to default values;
}

// And somewhere there
source.Cancel();
//or
token.ThrowIfCancellationRequested(); // try/ catch block requred.
Umka
la source