Pregunta ¿Hay un reemplazo basado en tareas para System.Threading.Timer?


Soy nuevo en las tareas de .NET 4.0 y no pude encontrar lo que pensé que sería un reemplazo basado en tareas o la implementación de un temporizador, p. una Tarea periódica. ¿Hay tal cosa?

Actualizar


74
2018-02-03 19:48


origen


Respuestas:


Depende de 4.5, pero esto funciona.

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

Obviamente, podría agregar una versión genérica que también tome argumentos. En realidad, esto es similar a otros enfoques sugeridos, ya que bajo el capó Task.Delay está utilizando un vencimiento del temporizador como fuente de finalización de tareas.


61
2018-05-22 18:38



ACTUALIZAR yo soy marcando la respuesta a continuación como la "respuesta", ya que esto es lo suficientemente viejo ahora que deberíamos estar usando el patrón async / await. No hay necesidad de rechazar esto más. LOL


Como Amy respondió, no hay una implementación periódica / temporizada basada en Tareas. Sin embargo, en base a mi ACTUALIZACIÓN original, hemos desarrollado esto en algo bastante útil y probado en producción. Pensé que compartiría:

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

Salida:

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

53
2018-02-18 21:15



No está exactamente en System.Threading.Tasks, pero Observable.Timer (o más simple Observable.Interval) de la biblioteca Reactive Extensions es probablemente lo que estás buscando.


12
2018-06-04 06:28



Hasta ahora, utilicé una tarea LongRunning TPL para el trabajo de fondo cíclico vinculado a la CPU en lugar del temporizador de subprocesamiento, porque:

  • la tarea TPL admite cancelación
  • el temporizador de subprocesamiento podría iniciar otro subproceso mientras el programa se está apagando causando posibles problemas con los recursos desechados
  • posibilidad de desbordamiento: el temporizador de subprocesamiento podría iniciar otro subproceso mientras el anterior todavía se está procesando debido a un largo trabajo inesperado (lo sé, se puede evitar deteniendo y reiniciando el temporizador)

Sin embargo, la solución TPL siempre reclama un hilo dedicado que no es necesario mientras se espera la siguiente acción (que es la mayor parte del tiempo). Me gustaría utilizar la solución propuesta de Jeff para realizar un trabajo cíclico vinculado a la CPU en segundo plano porque solo necesita un subproceso de subprocesos cuando hay trabajo por hacer, lo que es mejor para la escalabilidad (especialmente cuando el período de intervalo es grande).

Para lograr eso, sugeriría 4 adaptaciones:

  1. Añadir ConfigureAwait(false) al Task.Delay() para ejecutar el doWork acción en un hilo de grupo de subprocesos, de lo contrario doWork se realizará en el hilo de llamada que no es la idea de paralelismo
  2. Siga el patrón de cancelación lanzando una TaskCanceledException (¿aún se requiere?)
  3. Reenviar la cancelación. Tomado de doWork para permitirle cancelar la tarea
  4. Agregue un parámetro de tipo objeto para proporcionar información de estado de la tarea (como una tarea TPL)

Acerca del punto 2: No estoy seguro, ¿todavía espera la asincronización requiere el TaskCanceledExecption o es solo una mejor práctica?

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

Por favor, den sus comentarios a la solución propuesta ...

Actualización 2016-8-30

La solución anterior no llama inmediatamente doWork() pero comienza con await Task.Delay().ConfigureAwait(false) para lograr el interruptor de hilo para doWork(). La siguiente solución supera este problema al envolver el primer doWork() llamar en un Task.Run() y aguardarlo

A continuación se muestra el reemplazo mejorado async \ await para Threading.Timer que realiza trabajos cíclicos cancelables y es escalable (en comparación con la solución TPL) porque no ocupa ningún hilo mientras espera la siguiente acción.

Tenga en cuenta que, al contrario del temporizador, el tiempo de espera (period) es constante y no el tiempo del ciclo; el tiempo de ciclo es la suma del tiempo de espera y la duración de doWork() que puede variar

    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
2017-08-18 12:03



Me encontré con un problema similar y escribí un TaskTimer clase que devuelve un seris de tareas que se completa en el temporizador: 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
2017-09-05 03:10



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

Sencillo...


-1
2018-06-19 21:15