2010-06-06 8 views
10

J'essaye d'invoquer une méthode f() tous les temps t, mais si l'invocation précédente de f() n'est pas encore terminée, attendez jusqu'à ce que ce soit fini.Non-réentrant C# timer

J'ai lu un peu sur les minuteries disponibles, mais je n'ai trouvé aucun moyen de faire ce que je veux, sauf pour écrire manuellement tout. Toute aide sur la façon d'y parvenir sera appréciée, même si je crains de ne pas être en mesure de trouver une solution simple en utilisant des minuteurs.

Pour clarifier les choses, si t est une seconde, et f() exécute les durées arbitraires que j'ai écrit ci-dessous, puis:

Step Operation Time taken 
1  wait   1s 
2  f()   0.6s 
3  wait   0.4s (because f already took 0.6 seconds) 
4  f()   10s 
5  wait   0s (we're late) 
6  f()   0.3s 
7  wait   0.7s (we can disregard the debt from step 4) 

Notez que la nature de cette minuterie est que f() aura pas besoin d'être en sécurité en ce qui concerne la rentrée, et un pool de threads de taille 1 est suffisant ici.

+0

Le lien est mort. – astrowalker

+0

@astrowalker merci! Supprimé pour l'instant, car ce n'est pas vraiment nécessaire ici. – Oak

Répondre

8

Vous pouvez simplement utiliser un niveau "global" var (ou plus probablement, une propriété publique dans la même classe que f()) qui renvoie true si f() est déjà en cours d'exécution.

Donc, si f() était dans une classe nommée TimedEvent, la première chose que f() serait faire est de définir Running vrai

De cette façon, vos feux de minuterie à chaque seconde, lance alors l'événement chronométré si elle isnt déjà en cours d'exécution
if (!timedEvent.Running) timedEvent.f()

Vous avez indiqué que f() ne se répéterait pas immédiatement s'il prenait plus de temps que l'intervalle de temporisation. C'est un point juste. J'inclurais probablement une telle logique à l'intérieur de f() afin que Running reste vrai. Cela ressemblerait à ceci:

public void f(int t) // t is interval in seconds 
{ 
    this.running = true; 

    Stopwatch stopWatch = new Stopwatch(); 
    stopWatch.Start(); 

    do 
    { 
     stopwatch.Reset(); 

     // Do work here 

    } while (stopWatch.Elapsed.Seconds > t); // repeat if f() took longer than t 

    this.running = false; 
} 
+1

Si vous faites cela, méfiez-vous des conditions de course. –

+0

Vrai, mais questionneur a déclaré qu'il n'a pas besoin d'être multi-thread sûr – PaulG

+0

Très élégant, bien que cela signifie qu'un 2ème 'f' ne suivra pas immédiatement un 10,5 seconde' f', n'est-ce pas? Il attendra 0,5 secondes entre eux ... – Oak

3

Vous pouvez utiliser un temporisateur qui ne redémarre pas, puis redémarrer manuellement le temporisateur après la fin de la méthode.

Notez que cela entraînera un timing légèrement différent de ce que vous demandez. (Il y aura toujours un écart de temps entre t invocations)

Vous pouvez résoudre ce en définissant l'intervalle de lastTick + t - Now et d'exécuter la méthode immédiatement si c'est <= 0.

Méfiez-vous des conditions de course si vous devez arrêter le chronomètre.

1

Vous ne pouvez pas avoir de temporisateur pour vous appeler à intervalles réguliers. Tous les chronométreurs font vous rappeler au plus tôt l'heure demandée.

Certains compteurs sont mieux que d'autres (par exemple Windows.Forms.Timer est très erratique et peu fiable par rapport à System.Threading.Timer)

Pour arrêter votre minuterie étant appelé réentrant, une approche consiste à arrêter la timer pendant que votre méthode est en cours d'exécution. (Selon le type de minuteur que vous utilisez, vous pouvez l'arrêter et le redémarrer lorsque votre gestionnaire se termine, ou avec certains minuteurs, vous pouvez demander un rappel unique plutôt que de répéter les rappels, donc chaque exécution de votre gestionnaire met simplement en file d'attente l'appel suivant) .

Pour que la synchronisation soit relativement égale entre ces appels, vous pouvez enregistrer l'heure écoulée depuis la dernière exécution de votre gestionnaire et l'utiliser pour calculer le délai jusqu'à ce que l'événement suivant soit requis. par exemple. Si vous voulez être appelé une fois par seconde et que votre minuteur se termine à 1.02s, alors vous pouvez configurer le rappel suivant d'une durée de 0.98s pour tenir compte du fait que vous avez déjà "épuisé" une partie de la prochaine deuxième pendant votre traitement.

12

Utilisez un temporisateur System.Threading. Initialisez-le avec une période de Timeout.Infinite pour qu'il se comporte comme une minuterie unique. Lorsque f() se termine, appelez sa méthode Change() pour le recharger à nouveau.

+0

Voulez-vous dire définir la période à 'Timeout.Infinite'? – bavaza

0

Une solution simple:

private class Worker : IDisposable 
{ 
    private readonly TimeSpan _interval; 
    private WorkerContext _workerContext; 

    private sealed class WorkerContext 
    { 
     private readonly ManualResetEvent _evExit; 
     private readonly Thread _thread; 
     private readonly TimeSpan _interval; 

     public WorkerContext(ParameterizedThreadStart threadProc, TimeSpan interval) 
     { 
      _evExit = new ManualResetEvent(false); 
      _thread = new Thread(threadProc); 
      _interval = interval; 
     } 

     public ManualResetEvent ExitEvent 
     { 
      get { return _evExit; } 
     } 

     public TimeSpan Interval 
     { 
      get { return _interval; } 
     } 

     public void Run() 
     { 
      _thread.Start(this); 
     } 

     public void Stop() 
     { 
      _evExit.Set(); 
     } 

     public void StopAndWait() 
     { 
      _evExit.Set(); 
      _thread.Join(); 
     } 
    } 

    ~Worker() 
    { 
     Stop(); 
    } 

    public Worker(TimeSpan interval) 
    { 
     _interval = interval; 
    } 

    public TimeSpan Interval 
    { 
     get { return _interval; } 
    } 

    private void DoWork() 
    { 
     /* do your work here */ 
    } 

    public void Start() 
    { 
     var context = new WorkerContext(WorkThreadProc, _interval); 
     if(Interlocked.CompareExchange<WorkerContext>(ref _workerContext, context, null) == null) 
     { 
      context.Run(); 
     } 
     else 
     { 
      context.ExitEvent.Close(); 
      throw new InvalidOperationException("Working alredy."); 
     } 
    } 

    public void Stop() 
    { 
     var context = Interlocked.Exchange<WorkerContext>(ref _workerContext, null); 
     if(context != null) 
     { 
      context.Stop(); 
     } 
    } 

    private void WorkThreadProc(object p) 
    { 
     var context = (WorkerContext)p; 
     // you can use whatever time-measurement mechanism you want 
     var sw = new System.Diagnostics.Stopwatch(); 
     int sleep = (int)context.Interval.TotalMilliseconds; 
     while(true) 
     { 
      if(context.ExitEvent.WaitOne(sleep)) break; 

      sw.Reset(); 
      sw.Start(); 

      DoWork(); 

      sw.Stop(); 

      var time = sw.Elapsed; 
      if(time < _interval) 
       sleep = (int)(_interval - time).TotalMilliseconds; 
      else 
       sleep = 0; 
     } 
     context.ExitEvent.Close(); 
    } 

    public void Dispose() 
    { 
     Stop(); 
     GC.SuppressFinalize(this); 
    } 
} 
0

Comment l'utilisation de délégués à la méthode f(), les files d'attente à une pile, et éclater la pile comme chaque délégué complète? Vous avez toujours besoin de la minuterie, bien sûr.

0

Un fil simple est le moyen le plus simple d'y parvenir. Votre encore ne va pas être certain que votre appelé «précisément» quand vous voulez, mais il devrait être proche .... Aussi, vous pouvez décider si vous voulez ignorer les appels que devrait arriver ou tenter de rattraper ... Voici une routine d'aide simple pour créer le fil.

public static Thread StartTimer(TimeSpan interval, Func<bool> operation) 
{ 
    Thread t = new Thread(new ThreadStart(
     delegate() 
     { 
      DateTime when = DateTime.Now; 
      TimeSpan wait = interval; 
      while (true) 
      { 
       Thread.Sleep(wait); 
       if (!operation()) 
        return; 
       DateTime dt = DateTime.Now; 
       when += interval; 
       while (when < dt) 
        when += interval; 
       wait = when - dt; 
      } 
     } 
    )); 
    t.IsBackground = true; 
    t.Start(); 
    return t; 
} 
0

Pour le bénéfice des gens qui atterrissent ici la recherche de « réentrance »: (Je sais que cela peut être trop tard pour la question initiale) Si l'on est pas opposé à l'utilisation de bibliothèques open source qui fournissent déjà pour une telle fonctionnalité, j'ai réalisé avec succès une implémentation utilisant Quartz.NET Lorsque vous créez un travail et attachez un déclencheur, vous pouvez spécifier ce qui doit être fait si un déclencheur précédent n'a pas terminé l'exécution de son travail