web-dev-qa-db-fra.com

Meilleure façon de créer une fonction temporisée "run-once" en C #

J'essaie de créer une fonction qui prend une action et une temporisation, et exécute l'action après la temporisation. La fonction doit être non bloquante. La fonction doit être thread-safe. Je veux aussi vraiment, vraiment éviter Thread.Sleep ().

Jusqu'ici, le mieux que je puisse faire est la suivante:

long currentKey = 0;
ConcurrentDictionary<long, Timer> timers = new ConcurrentDictionary<long, Timer>();

protected void Execute(Action action, int timeout_ms)
{
    long currentKey = Interlocked.Increment(ref currentKey);
    Timer t = new Timer(
      (key) =>
         {
           action();
           Timer lTimer;
           if(timers.TryRemove((long)key, out lTimer))
           {
               lTimer.Dispose();
           }
         }, currentKey, Timeout.Infinite, Timeout.Infinite
      );

     timers[currentKey] = t;
     t.Change(timeout_ms, Timeout.Infinite);
}

Le problème est que l'appel de Dispose () à partir du rappel lui-même ne peut pas être bon. Je ne sais pas s’il est sûr de "tomber" de la fin, c’est-à-dire que les minuteries sont considérées comme en direct pendant que leurs lambdas sont en cours d’exécution, mais même si tel était le cas, je préférerais en disposer correctement.

Le "feu une fois avec un retard" semble être un problème si commun qu’il devrait exister un moyen simple de le faire, probablement une autre bibliothèque de System.Pas de choses qui me manque, mais pour le moment la seule solution à laquelle je peux penser est la modification de ci-dessus avec une tâche de nettoyage dédiée exécutée sur un intervalle. Aucun conseil?

41
Chuu

Je ne sais pas quelle version de C # vous utilisez. Mais je pense que vous pourriez accomplir cela en utilisant la bibliothèque de tâches. Cela ressemblerait alors à quelque chose comme ça.

public class PauseAndExecuter
{
    public async Task Execute(Action action, int timeoutInMilliseconds)
    {
        await Task.Delay(timeoutInMilliseconds);
        action();
    }
}
65
treze

Il n’existe rien dans .Net 4 qui permette de le faire correctement. Thread.Sleep ou même AutoResetEvent.WaitOne (délai d'expiration) ne sont pas bons - ils vont bloquer les ressources du pool de threads, j'ai été brûlé en essayant ça! 

La solution la plus légère consiste à utiliser une minuterie, en particulier si vous devez exécuter de nombreuses tâches.

Commencez par créer une classe de tâches planifiée simple:

class ScheduledTask
{
    internal readonly Action Action;
    internal System.Timers.Timer Timer;
    internal EventHandler TaskComplete;

    public ScheduledTask(Action action, int timeoutMs)
    {
        Action = action;
        Timer = new System.Timers.Timer() { Interval = timeoutMs };
        Timer.Elapsed += TimerElapsed;            
    }

    private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Timer.Stop();
        Timer.Elapsed -= TimerElapsed;
        Timer = null;

        Action();
        TaskComplete(this, EventArgs.Empty);
    }
}

Ensuite, créez une classe de planificateur - là encore, très simple:

class Scheduler
{        
    private readonly ConcurrentDictionary<Action, ScheduledTask> _scheduledTasks = new ConcurrentDictionary<Action, ScheduledTask>();

    public void Execute(Action action, int timeoutMs)
    {
        var task = new ScheduledTask(action, timeoutMs);
        task.TaskComplete += RemoveTask;
        _scheduledTasks.TryAdd(action, task);
        task.Timer.Start();
    }

    private void RemoveTask(object sender, EventArgs e)
    {
        var task = (ScheduledTask) sender;
        task.TaskComplete -= RemoveTask;
        ScheduledTask deleted;
        _scheduledTasks.TryRemove(task.Action, out deleted);
    }
}

Il peut être appelé comme suit - et il est très léger:

var scheduler = new Scheduler();

scheduler.Execute(() => MessageBox.Show("hi1"), 1000);
scheduler.Execute(() => MessageBox.Show("hi2"), 2000);
scheduler.Execute(() => MessageBox.Show("hi3"), 3000);
scheduler.Execute(() => MessageBox.Show("hi4"), 4000);
26
James Harcourt

J'utilise cette méthode pour planifier une tâche à une heure précise:

public void ScheduleExecute(Action action, DateTime ExecutionTime)
{
    Task WaitTask = Task.Delay(ExecutionTime.Subtract(DateTime.Now).TotalMilliseconds);
    WaitTask.ContinueWith(() => action());
    WaitTask.Start();
}

Il convient de noter que cela ne fonctionne que pendant environ 24 jours en raison de la valeur int32 max.

4
VoteCoffee

Mon exemple:

void startTimerOnce()
{
   Timer tmrOnce = new Timer();
   tmrOnce.Tick += tmrOnce_Tick;
   tmrOnce.Interval = 2000;
   tmrOnce.Start();
}

void tmrOnce_Tick(object sender, EventArgs e)
{
   //...
   ((Timer)sender).Dispose();
}
4
Nikolay Khilyuk

Le modèle que vous avez, utilisant une minuterie à prise unique, est certainement la voie à suivre. Vous ne voulez certainement pas créer un nouveau fil pour chacun d'entre eux. Vous pourriez avoir un seul thread et une file d'attente prioritaire d'actions programmées à temps, mais c'est une complexité inutile.

Appeler Dispose dans le rappel n'est probablement pas une bonne idée, bien que je serais tenté d'essayer. Je semble me souvenir de l'avoir fait dans le passé et cela a bien fonctionné. Mais je l'admettrai, c'est un peu n'importe quoi.

Vous pouvez simplement retirer le chronomètre de la collection et ne pas le jeter. Sans référence à l'objet, il sera éligible pour le nettoyage de la mémoire, ce qui signifie que la méthode Dispose sera sera appelée par le finaliseur. Juste pas aussi opportun que vous pourriez aimer. Mais cela ne devrait pas être un problème. Vous ne faites que laisser couler une poignée pendant une brève période. Tant que vous ne gardez pas des milliers de ces objets sans rien faire pendant une longue période, cela ne posera pas de problème.

Une autre option consiste à avoir une file d'attente de minuteries qui reste allouée mais est désactivée (c'est-à-dire que leur délai d'attente et leurs intervalles sont définis sur Timeout.Infinite). Lorsque vous avez besoin d'un minuteur, vous en tirez un dans la file d'attente, vous le définissez et vous l'ajoutez à votre collection. Une fois le délai expiré, vous effacez la minuterie et vous la remettez en file d'attente. Vous pouvez agrandir la file d'attente de manière dynamique si vous devez, et vous pouvez même la nettoyer de temps en temps.

Cela vous évitera de laisser filtrer une minuterie pour chaque événement. Au lieu de cela, vous aurez un pool de minuteurs (un peu comme le pool de threads, non?).

2
Jim Mischel

Si vous ne vous souciez pas de la granularité temporelle, vous pouvez créer un minuteur qui se déclenche toutes les secondes et recherche les actions expirées devant être mises en file d'attente sur le ThreadPool. Utilisez simplement la classe chronomètre pour vérifier le délai.

Vous pouvez utiliser votre approche actuelle, sauf que votre dictionnaire aura un chronomètre comme clé et une action comme valeur. Ensuite, il vous suffit de parcourir l'ensemble des KeyValuePairs et de trouver le chronomètre qui arrive à expiration, de mettre l'action en file d'attente, puis de le supprimer. Cependant, vous obtiendrez de meilleures performances et une plus grande utilisation de la mémoire grâce à une LinkedList (puisque vous énumérerez le tout à chaque fois et qu'il sera plus facile de supprimer un élément).

2

La documentation indique clairement que System.Timers.Timer a la propriété AutoReset créée uniquement pour ce que vous demandez:

https://msdn.Microsoft.com/en-us/library/system.timers.timer.autoreset(v=vs.110).aspx

1
Lu4

Pourquoi ne pas simplement invoquer votre paramètre d'action lui-même dans une action asynchrone?

Action timeoutMethod = () =>
  {
       Thread.Sleep(timeout_ms);
       action();
  };

timeoutMethod.BeginInvoke();
1
Tejs

le code de treze fonctionne très bien. Cela pourrait aider ceux qui doivent utiliser d'anciennes versions .NET:

private static volatile List<System.Threading.Timer> _timers = new List<System.Threading.Timer>();
private static object lockobj = new object();
public static void SetTimeout(Action action, int delayInMilliseconds)
{
    System.Threading.Timer timer = null;
    var cb = new System.Threading.TimerCallback((state) =>
    {
        lock (lockobj)
            _timers.Remove(timer);
        timer.Dispose();
        action();
    });
    lock (lockobj)
        _timers.Add(timer = new System.Threading.Timer(cb, null, delayInMilliseconds, System.Threading.Timeout.Infinite));
}
1
Koray

Cela semble fonctionner pour moi. Cela me permet d’appeler _connection.Start () après un délai de 15 secondes. Le paramètre -1 milliseconde indique simplement que ne pas répéter.

// Instance or static holder that won't get garbage collected (thanks chuu)
System.Threading.Timer t;

// Then when you need to delay something
var t = new System.Threading.Timer(o =>
            {
                _connection.Start(); 
            },
            null,
            TimeSpan.FromSeconds(15),
            TimeSpan.FromMilliseconds(-1));
0
naskew

Utilisez le cadre réactif de Microsoft (NuGet "System.Reactive") et procédez comme suit:

protected void Execute(Action action, int timeout_ms)
{
    Scheduler.Default.Schedule(TimeSpan.FromMilliseconds(timeout_ms), action);
}
0
Enigmativity