Existe-t-il un remplacement basé sur les tâches pour System.Threading.Timer?

88

Je suis nouveau dans les tâches de .Net 4.0 et je n'ai pas pu trouver ce que je pensais être un remplacement basé sur une tâche ou une implémentation d'un minuteur, par exemple une tâche périodique. Existe-t-il une telle chose?

Mise à jour J'ai trouvé ce que je pense être une solution à mes besoins qui est d'encapsuler la fonctionnalité "Timer" dans une tâche avec des tâches enfants qui profitent toutes du CancellationToken et renvoie la tâche pour pouvoir participer à d'autres étapes de tâche.

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      
Jim
la source
7
Vous devez utiliser un minuteur dans la tâche au lieu d'utiliser le mécanisme Thread.Sleep. C'est plus efficace.
Yoann. B

Réponses:

84

Cela dépend de 4.5, mais cela fonctionne.

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

Évidemment, vous pouvez ajouter une version générique qui prend également des arguments. Ceci est en fait similaire à d'autres approches suggérées car sous le capot, Task.Delay utilise une expiration du minuteur comme source d'achèvement de tâche.

Jeff
la source
1
Je suis passé à cette approche tout à l'heure. Mais j'appelle conditionnellement action()avec une répétition de !cancelToken.IsCancellationRequested. C'est mieux, non?
HappyNomad le
3
Merci pour cela - nous utilisons la même chose mais avons déplacé le délai jusqu'à la fin de l'action (cela nous semble plus logique car nous devons appeler l'action immédiatement, puis la répéter après x)
Michael Parker
1
Merci pour cela. Mais ce code ne fonctionnera pas "toutes les X heures", il fonctionnera "toutes les X heures + heure d' actionexécution", j'ai raison?
Alex
Correct. Vous auriez besoin de quelques calculs si vous voulez tenir compte du temps d'exécution. Cependant, cela peut devenir délicat si le temps d'exécution dépasse votre période, etc ...
Jeff
57

MISE À JOUR Je marque la réponse ci-dessous comme "réponse" car c'est assez vieux maintenant que nous devrions utiliser le modèle async / wait. Plus besoin de voter contre cela. LOL


Comme Amy a répondu, il n'y a pas d'implémentation périodique / minuterie basée sur les tâches. Cependant, sur la base de ma mise à jour originale, nous avons fait évoluer cela en quelque chose d'assez utile et testé en production. Je pensais que je partagerais:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

Production:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .
Jim
la source
1
Cela ressemble à du bon code, mais je me demande si c'est nécessaire maintenant qu'il y a les mots-clés async / await. Comment votre approche se compare-t-elle à celle ici: stackoverflow.com/a/14297203/122781 ?
HappyNomad
1
@HappyNomad, il semble que la classe PeriodicTaskFactory pourrait tirer parti de async / await pour les applications ciblant .Net 4.5 mais pour nous, nous ne pouvons pas encore passer à .Net 4.5. En outre, PeriodicTaskFactory fournit des mécanismes supplémentaires de terminaison de "minuterie" tels que le nombre maximal d'itérations et la durée maximale, ainsi qu'un moyen de garantir que chaque itération peut attendre la dernière itération. Mais je chercherai à adapter cela pour utiliser async / await lorsque nous passerons à .Net 4.5
Jim
4
+1 J'utilise votre classe maintenant, merci. Pour que cela fonctionne bien avec le thread de l'interface utilisateur, je dois appeler TaskScheduler.FromCurrentSynchronizationContext()avant de configurer mainAction. Je passe ensuite le planificateur résultant MainPeriodicTaskActionpour qu'il crée le subTaskavec.
HappyNomad
2
Je ne suis pas sûr, c'est une bonne idée de bloquer un fil, quand il peut faire un travail utile. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Ensuite, vous utilisez un Timer, vous attendez dans le matériel, donc pas de threads dépensés. Mais dans votre solution, les threads sont dépensés pour rien.
RollingStone
2
@rollingstone Je suis d'accord. Je pense que cette solution va largement à l'encontre de l'objectif du comportement de type asynchrone. Il vaut mieux utiliser une minuterie et ne pas gaspiller le fil. Cela donne simplement l'apparence de l'asynchrone sans aucun des avantages.
Jeff
12

Ce n'est pas exactement dans System.Threading.Tasks, mais Observable.Timer(ou plus simple Observable.Interval) de la bibliothèque Reactive Extensions est probablement ce que vous recherchez.

mstone
la source
1
Par exemple, Observable.Interval (TimeSpan.FromSeconds (1)). Subscribe (v => Debug.WriteLine (v));
Martin Capodici
1
Bien, mais ces constructions réactives sont-elles réalisables?
Shmil The Cat
9

Jusqu'à présent, j'utilisais une tâche TPL LongRunning pour le travail en arrière-plan lié au processeur cyclique au lieu du minuteur de thread, car:

  • la tâche TPL prend en charge l'annulation
  • le minuteur de thread peut démarrer un autre thread pendant que le programme s'arrête, ce qui peut causer des problèmes avec les ressources supprimées
  • chance de dépassement: le minuteur de thread pourrait démarrer un autre thread alors que le précédent est toujours en cours de traitement en raison d'un long travail inattendu (je sais, cela peut être évité en arrêtant et en redémarrant le minuteur)

Cependant, la solution TPL revendique toujours un thread dédié qui n'est pas nécessaire en attendant la prochaine action (ce qui est la plupart du temps). Je voudrais utiliser la solution proposée par Jeff pour effectuer un travail cyclique lié au processeur en arrière-plan car il n'a besoin d'un thread de pool de threads que lorsqu'il y a du travail à faire, ce qui est meilleur pour l'évolutivité (en particulier lorsque la période d'intervalle est grande).

Pour y parvenir, je proposerais 4 adaptations:

  1. Ajouter ConfigureAwait(false) à Task.Delay()pour exécuter l' doWorkaction sur un thread de pool de threads, sinondoWork sera effectué sur le thread appelant ce qui n'est pas l'idée du parallélisme
  2. Tenez-vous en au modèle d'annulation en lançant une TaskCanceledException (toujours requise?)
  3. Transférer le jeton d'annulation à doWork pour lui permettre d'annuler la tâche
  4. Ajouter un paramètre de type objet pour fournir des informations sur l'état de la tâche (comme une tâche TPL)

À propos du point 2 Je ne suis pas sûr, est-ce que async wait nécessite toujours la TaskCanceledExecption ou est-ce simplement la meilleure pratique?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

Veuillez faire part de vos commentaires sur la solution proposée ...

Mise à jour 2016-8-30

La solution ci-dessus n'appelle pas immédiatement doWork()mais commence par await Task.Delay().ConfigureAwait(false)obtenir le commutateur de thread pour doWork(). La solution ci-dessous résout ce problème en enveloppant le premier doWork()appel dans unTask.Run() et en l'attendant.

Vous trouverez ci-dessous le remplacement amélioré async \ await pour Threading.Timer qui effectue un travail cyclique annulable et est évolutif (par rapport à la solution TPL) car il n'occupe aucun thread en attendant l'action suivante.

Notez que contrairement au Timer, le temps d'attente ( period) est constant et non le temps de cycle; le temps de cycle est la somme du temps d'attente et dont la durée doWork()peut varier.

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }
Erik Stroeken
la source
L'utilisation ConfigureAwait(false)planifie la poursuite de la méthode vers le pool de threads, donc cela ne résout pas vraiment le deuxième point concernant le minuteur de thread. Je ne pense pas non plus que ce taskStatesoit nécessaire; La capture des variables lambda est plus flexible et sécurisée.
Stephen Cleary
1
Ce que je veux vraiment faire, c'est échanger await Task.Delay()et doWork()donc doWork()s'exécuter immédiatement au démarrage. Mais sans astuce doWork(), exécuterait le thread appelant la première fois et le bloquerait. Stephen, avez-vous une solution à ce problème?
Erik Stroeken
1
Le moyen le plus simple est de simplement envelopper le tout dans un fichier Task.Run.
Stephen Cleary
Oui, mais je peux simplement revenir à la solution TPL que j'utilise maintenant qui revendique un thread tant que la boucle est en cours d'exécution et est donc moins évolutive que cette solution.
Erik Stroeken
1

J'avais besoin de déclencher les tâches asynchrones récurrentes à partir d'une méthode synchrone.

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

Ceci est une adaptation de la réponse de Jeff. Il est modifié pour prendre un. Func<Task> Il s'assure également que la période correspond à la fréquence à laquelle elle est exécutée en déduisant le temps d'exécution de la tâche de la période du délai suivant.

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}
chris31389
la source
0

J'ai rencontré un problème similaire et j'ai écrit une TaskTimerclasse qui renvoie une série de tâches qui se terminent sur la minuterie: https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}
Ivan Krivyakov
la source
-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

Facile...

nim
la source