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

91

Soy nuevo en las tareas de .Net 4.0 y no pude encontrar lo que pensé que sería un reemplazo o implementación de un temporizador basado en tareas, por ejemplo, una tarea periódica. ¿Existe tal cosa?

Actualización Se me ocurrió lo que creo que es una solución a mis necesidades que consiste en envolver la funcionalidad "Temporizador" dentro de una Tarea con Tareas secundarias, todas aprovechando el CancellationToken y devuelve la Tarea para poder participar en más pasos de Tarea.

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
fuente
7
Debe usar un temporizador dentro de la tarea en lugar de usar el mecanismo Thread.Sleep. Es más eficiente.
Yoann. B

Respuestas:

87

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 tenga argumentos. En realidad, esto es similar a otros enfoques sugeridos, ya que bajo el capó Task.Delay usa un vencimiento del temporizador como fuente de finalización de tareas.

Jeff
fuente
1
Cambié a este enfoque hace un momento. Pero llamo condicionalmente action()con una repetición de !cancelToken.IsCancellationRequested. Eso es mejor, ¿verdad?
HappyNomad
3
Gracias por esto, estamos usando lo mismo, pero hemos movido el retraso hasta después de la acción (tiene más sentido para nosotros, ya que necesitamos llamar a la acción inmediatamente y luego repetir después de x)
Michael Parker
2
Gracias por esto. Pero este código no se ejecutará "cada X horas", se ejecutará "cada X horas + tiempo de actionejecución", ¿verdad?
Alex
Correcto. Necesitaría algo de matemáticas si quiere tener en cuenta el tiempo de ejecución. Sin embargo, eso puede ser complicado si el tiempo de ejecución excede su período, etc.
Jeff
57

ACTUALIZAR Estoy marcando la respuesta a continuación como la "respuesta" ya que esto es lo suficientemente antiguo ahora que deberíamos usar el patrón async / await. Ya no es necesario rechazar esto. Jajaja


Como respondió Amy, no existe una implementación periódica / temporizada basada en tareas. Sin embargo, según mi ACTUALIZACIÓN original, lo hemos convertido 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 . . .
Jim
fuente
1
Esto parece un gran código, pero me pregunto si es necesario ahora que existen las palabras clave async / await. ¿Cómo se compara su enfoque con el de aquí: stackoverflow.com/a/14297203/122781 ?
HappyNomad
1
@HappyNomad, parece que la clase PeriodicTaskFactory podría aprovechar async / await para aplicaciones dirigidas a .Net 4.5, pero para nosotros, todavía no podemos pasar a .Net 4.5. Además, PeriodicTaskFactory proporciona algunos mecanismos de terminación de "temporizador" adicionales, como el número máximo de iteraciones y la duración máxima, además de proporcionar una forma de garantizar que cada iteración pueda esperar en la última iteración. Pero buscaré adaptar esto para usar async / await cuando nos mudemos a .Net 4.5
Jim
4
+1 Estoy usando tu clase ahora, gracias. Sin embargo, para que funcione bien con el hilo de la interfaz de usuario, tengo que llamar TaskScheduler.FromCurrentSynchronizationContext()antes de configurar mainAction. Luego paso el programador resultante MainPeriodicTaskActionpara que cree el archivo subTaskwith.
HappyNomad
2
No estoy seguro, esta es una buena idea para bloquear un hilo, cuando puede hacer un trabajo útil. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Luego usa un Timer, espera en el hardware, por lo que no se gastan hilos. Pero en su solución, los hilos se gastan en vano.
RollingStone
2
@rollingstone estoy de acuerdo. Creo que esta solución frustra en gran medida el propósito del comportamiento asíncrono. Es mucho mejor usar un temporizador y no desperdiciar el hilo. Esto solo da la apariencia de async sin ninguno de los beneficios.
Jeff
12

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

mstone
fuente
1
Por ejemplo, Observable.Interval (TimeSpan.FromSeconds (1)). Subscribe (v => Debug.WriteLine (v));
Martin Capodici
1
Bien, pero ¿son cancalables esas construcciones reactivas?
Shmil The Cat
9

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

  • la tarea TPL admite la cancelación
  • el temporizador de subprocesos podría iniciar otro subproceso mientras el programa se cierra, lo que causa posibles problemas con los recursos desechados
  • posibilidad de desbordamiento: el temporizador de enhebrado podría iniciar otro hilo mientras el anterior todavía se está procesando debido a un trabajo largo 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 usar la solución propuesta de Jeff para realizar un trabajo cíclico vinculado a la CPU en segundo plano porque solo necesita un hilo de subprocesos cuando hay trabajo por hacer que es mejor para la escalabilidad (especialmente cuando el período de intervalo es grande).

Para lograrlo, sugeriría 4 adaptaciones:

  1. Agregue ConfigureAwait(false)para Task.Delay()ejecutar la doWorkacción en un subproceso del grupo de subprocesos; de lo contrario doWork, se realizará en el subproceso de llamada que no es la idea de paralelismo
  2. Siga el patrón de cancelación lanzando una TaskCanceledException (¿todavía es necesario?)
  3. Reenvíe el CancellationToken para doWorkhabilitarlo para cancelar la tarea
  4. Agregue un parámetro de tipo objeto para proporcionar información sobre el estado de la tarea (como una tarea TPL)

Acerca del punto 2, no estoy seguro, ¿async await aún requiere 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, dé sus comentarios a la solución propuesta ...

Actualización 2016-8-30

La solución anterior no llama inmediatamente, doWork()sino que comienza con await Task.Delay().ConfigureAwait(false)para lograr el cambio de hilo para doWork(). La siguiente solución resuelve este problema al encapsular la primera doWork()llamada en a Task.Run()y aguardarla.

A continuación se muestra el reemplazo async \ await mejorado Threading.Timerque realiza un trabajo cíclico cancelable 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 que con el temporizador, el tiempo de espera ( period) es constante y no el tiempo de ciclo; el tiempo del ciclo es la suma del tiempo de espera y la duración del doWork()cual 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);
    }
Erik Stroeken
fuente
El uso ConfigureAwait(false)programará la continuación del método en el grupo de subprocesos, por lo que realmente no resuelve el segundo punto con respecto al temporizador de subprocesos. Tampoco creo que taskStatesea ​​necesario; La captura de variables lambda es más flexible y segura para los tipos.
Stephen Cleary
1
Lo que realmente quiero hacer es intercambiar await Task.Delay()y, doWork()por doWork()lo tanto, se ejecutaría inmediatamente durante el inicio. Pero sin algún truco doWork()se ejecutaría en el hilo de llamada la primera vez y lo bloquearía. Stephen, ¿tienes una solución para ese problema?
Erik Stroeken
1
La forma más sencilla es simplemente envolver todo en un Task.Run.
Stephen Cleary
Sí, pero luego puedo volver a la solución TPL que uso ahora, que reclama un hilo siempre que el bucle se esté ejecutando y, por lo tanto, sea menos escalable que esta solución.
Erik Stroeken
1

Necesitaba activar las tareas asincrónicas recurrentes desde un método sincrónico.

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

Esta es una adaptación de la respuesta de Jeff. También se Func<Task> asegura de que el período sea la frecuencia con la que se ejecuta deduciendo el tiempo de ejecución de la tarea del período para el siguiente retraso.

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
fuente
0

Me encontré con un problema similar y escribí una TaskTimerclase que devuelve una serie de tareas que se completan 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();
    }
}
Ivan Krivyakov
fuente
-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; }
}

Sencillo...

nim
fuente