web-dev-qa-db-fra.com

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

Je suis nouveau dans les tâches .NET 4.0 et je n’ai pas pu trouver ce que je pensais être un remplacement ou une implémentation basé sur une tâche, par exemple une minuterie, par exemple. une tâche périodique. Une telle chose existe t elle?

Update J'ai proposé une solution qui, à mon avis, consiste à intégrer la fonctionnalité "Minuterie" dans une tâche avec des tâches enfant tirant toutes parti de l'AnnulationToken et renvoyant la tâche. pouvoir participer à d'autres étapes de la 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);
}      
77
Jim

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);
     }
}

Vous pouvez évidemment ajouter une version générique prenant également des arguments. C’est en fait similaire aux autres approches suggérées, car sous le capot, Task.Delay utilise une expiration de la minuterie comme source d’achèvement des tâches.

69
Jeff

UPDATE Je suis marquant la réponse ci-dessous en tant que "réponse" car elle est suffisamment ancienne pour que nous devrions utiliser le modèle async/wait Plus besoin de baisser la voix. LOL


Comme Amy a répondu, il n'y a pas d'implémentation périodique/minuterie basée sur Tasked. Cependant, basé sur ma mise à jour originale, nous avons fait évoluer cela en quelque chose d'assez utile et testé en production. Je pensais partager:

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();
        }
    }
}

Sortie:

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 . . .
56
Jim

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.

12
mstone

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

  • la tâche TPL prend en charge l'annulation 
  • le minuteur de threading peut démarrer un autre thread pendant l'arrêt du programme, ce qui peut causer des problèmes avec les ressources supprimées
  • risque de dépassement: le minuteur de threading peut 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 que cela peut être évité en arrêtant et en redémarrant le minuteur)

Cependant, la solution TPL réclame toujours un thread dédié qui n'est pas nécessaire pendant l'attente de l'action suivante (qui est la plupart du temps). Je voudrais utiliser la solution proposée par Jeff pour effectuer un travail cyclique lié au processeur sur l’arrière-plan car il n’a besoin d’un thread threadpool que s’il ya du travail à faire, ce qui est préférable pour l’évolutivité (surtout lorsque la période d’intervalle est grande).

Pour y parvenir, je proposerais 4 adaptations:

  1. Ajoutez ConfigureAwait(false) à la Task.Delay() pour exécuter l'action doWork sur un thread de pool de threads, sinon doWork sera exécuté sur le thread appelant, ce qui n'est pas l'idée de parallélisme.
  2. Respectez le modèle d'annulation en lançant une exception TaskCanceledException (toujours nécessaire?)
  3. Transmettez le CancellationToken à 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 sais pas si l'attente asynchrone requiert toujours la TaskCanceledExecption ou s'agit-il simplement d'une 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);
    }

S'il vous plaît donner vos commentaires à 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) pour obtenir le commutateur de thread pour doWork(). La solution ci-dessous résout ce problème en encapsulant le premier appel doWork() dans une Task.Run() et l'attendre.

Vous trouverez ci-dessous le remplacement async\wait amélioré 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 temporisateur, 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 de la durée de doWork() qui 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);
    }
8
Erik Stroeken

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 dans un Func<Task>. Il s'assure également que la période est 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}");
    }
}
0
chris31389

J'ai rencontré un problème similaire et écrit une classe TaskTimer qui renvoie une série de tâches qui se terminent à 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();
    }
}
0
Ivan Krivyakov