Esiste un sostituto basato su attività per System.Threading.Timer?


88

Sono nuovo alle attività di .Net 4.0 e non sono stato in grado di trovare ciò che pensavo fosse una sostituzione basata su attività o l'implementazione di un timer, ad esempio un'attività periodica. C'è una cosa del genere?

Aggiornamento Mi è venuta in mente quella che penso sia una soluzione alle mie esigenze che è quella di racchiudere la funzionalità "Timer" all'interno di un Task con Task figlio tutti sfruttando il CancellationToken e restituisce il Task per poter partecipare a ulteriori passaggi del Task.

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

7
Dovresti usare un timer all'interno dell'attività invece di usare il meccanismo Thread.Sleep. È più efficiente.
Yoann. B

Risposte:


84

Dipende da 4.5, ma funziona.

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

Ovviamente potresti aggiungere una versione generica che accetta anche argomenti. Questo è in realtà simile ad altri approcci suggeriti poiché sotto il cofano Task.Delay utilizza una scadenza del timer come fonte di completamento dell'attività.


1
Sono passato a questo approccio solo ora. Ma chiamo condizionatamente action()con una ripetizione di !cancelToken.IsCancellationRequested. Va meglio, vero?
HappyNomad

3
Grazie per questo - stiamo usando lo stesso ma abbiamo spostato il ritardo fino a dopo l'azione (ha più senso per noi poiché dobbiamo chiamare l'azione immediatamente e poi ripetere dopo x)
Michael Parker

1
Grazie per questo. Ma questo codice non verrà eseguito "ogni X ore" verrà eseguito "ogni X ore + tempo di actionesecuzione", giusto?
Alex

Corretta. Avresti bisogno di matematica se vuoi tenere conto del tempo di esecuzione. Tuttavia, ciò può diventare complicato se il tempo di esecuzione supera il tuo periodo, ecc ...
Jeff

57

UPDATE Sto segnando la risposta qui sotto come "risposta" poiché questo è abbastanza vecchio, ora che dovremmo usare l'async / modello attendono. Non c'è più bisogno di downvote questo. LOL


Come ha risposto Amy, non esiste un'implementazione periodica / timer basata su Tasked. Tuttavia, in base al mio UPDATE originale, lo abbiamo trasformato in qualcosa di abbastanza utile e testato in produzione. Ho pensato di condividere:

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

Produzione:

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

1
Sembra un ottimo codice, ma mi chiedo se sia necessario ora che ci sono le parole chiave async / await. Come si confronta il tuo approccio con quello qui: stackoverflow.com/a/14297203/122781 ?
HappyNomad

1
@HappyNomad, sembra che la classe PeriodicTaskFactory potrebbe trarre vantaggio da async / await per applicazioni destinate a .Net 4.5 ma per noi non possiamo ancora passare a .Net 4.5. Inoltre, PeriodicTaskFactory fornisce alcuni meccanismi di terminazione "timer" aggiuntivi come il numero massimo di iterazioni e la durata massima, oltre a fornire un modo per garantire che ogni iterazione possa attendere l'ultima iterazione. Ma cercherò di adattare questo per utilizzare async / await quando passeremo a .Net 4.5
Jim

4
+1 Sto usando la tua classe ora, grazie. Per farlo funzionare bene con il thread dell'interfaccia utente, però, devo chiamare TaskScheduler.FromCurrentSynchronizationContext()prima di impostare mainAction. Quindi passo lo scheduler risultante inMainPeriodicTaskAction per creare il file subTaskwith.
HappyNomad

2
Non sono sicuro, questa è una buona idea per bloccare un thread, quando può fare un lavoro utile. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Quindi usi un timer, aspetti nell'hardware, quindi nessun thread è speso. Ma nella tua soluzione, i thread vengono spesi per niente.
RollingStone

2
@rollingstone sono d'accordo. Penso che questa soluzione sconfigga ampiamente lo scopo del comportamento asincrono. Molto meglio usare un timer e non sprecare il filo. Questo sta solo dando l'aspetto di asincrono senza nessuno dei vantaggi.
Jeff


9

Fino ad ora ho utilizzato un'attività TPL LongRunning per il lavoro in background ciclico associato alla CPU invece del timer di threading, perché:

  • l'attività TPL supporta l'annullamento
  • il timer di threading potrebbe avviare un altro thread mentre il programma si sta chiudendo causando possibili problemi con le risorse eliminate
  • possibilità di sovraccarico: il timer di threading potrebbe avviare un altro thread mentre il precedente è ancora in fase di elaborazione a causa di un lungo lavoro inaspettato (lo so, può essere prevenuto arrestando e riavviando il timer)

Tuttavia, la soluzione TPL richiede sempre un thread dedicato che non è necessario in attesa dell'azione successiva (che è la maggior parte delle volte). Vorrei utilizzare la soluzione proposta di Jeff per eseguire il lavoro ciclico legato alla CPU in background perché ha bisogno solo di un thread di threadpool quando c'è del lavoro da fare che è meglio per la scalabilità (specialmente quando il periodo di intervallo è grande).

Per ottenere ciò, suggerirei 4 adattamenti:

  1. Aggiungi ConfigureAwait(false)a Task.Delay()per eseguire l' doWorkazione su un thread del pool di thread, altrimenti doWorkverrà eseguita sul thread chiamante che non è l'idea di parallelismo
  2. Attenersi al modello di cancellazione lanciando un'eccezione TaskCanceledException (ancora necessaria?)
  3. Inoltrare il CancellationToken a doWorkper abilitarlo all'annullamento dell'attività
  4. Aggiungi un parametro di tipo oggetto per fornire informazioni sullo stato dell'attività (come un'attività TPL)

Riguardo al punto 2, non sono sicuro, l'asincronizzazione attende ancora richiede TaskCanceledExecption o è solo una best practice?

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

Si prega di commentare la soluzione proposta ...

Aggiornamento 2016-8-30

La soluzione precedente non chiama immediatamente doWork()ma inizia con await Task.Delay().ConfigureAwait(false)per ottenere il thread switch per doWork(). La soluzione seguente risolve questo problema avvolgendo la prima doWork()chiamata in una Task.Run()e aspettandola.

Di seguito è riportato il sostituto async \ await migliorato Threading.Timerche esegue un lavoro ciclico cancellabile ed è scalabile (rispetto alla soluzione TPL) perché non occupa alcun thread in attesa dell'azione successiva.

Notare che al contrario del Timer, il tempo di attesa ( period) è costante e non il tempo di ciclo; il tempo di ciclo è la somma del tempo di attesa e la cui durata doWork()può variare.

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

L'utilizzo ConfigureAwait(false)pianificherà la continuazione del metodo nel pool di thread, quindi non risolve davvero il secondo punto relativo al timer di threading. Inoltre non penso taskStatesia necessario; l'acquisizione della variabile lambda è più flessibile e indipendente dai tipi.
Stephen Cleary

1
Quello che voglio veramente fare è scambiare await Task.Delay()e doWork()quindi doWork()verrà eseguito immediatamente durante l'avvio. Ma senza qualche trucco doWork()verrebbe eseguito la prima volta sul thread chiamante e lo bloccherebbe. Stephen, hai una soluzione per quel problema?
Erik Stroeken

1
Il modo più semplice è avvolgere il tutto in un file Task.Run.
Stephen Cleary

Sì, ma poi posso semplicemente tornare alla soluzione TPL che uso ora che rivendica un thread fintanto che il ciclo è in esecuzione e quindi è meno scalabile di questa soluzione.
Erik Stroeken

1

Avevo bisogno di attivare le attività asincrone ricorrenti da un metodo sincrono.

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

Questo è un adattamento della risposta di Jeff. Viene modificato per includere a. Func<Task> Inoltre, garantisce che il periodo corrisponda alla frequenza con cui viene eseguito sottraendo il tempo di esecuzione dell'attività dal periodo per il ritardo successivo.

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

Mi sono imbattuto in un problema simile e ho scritto una TaskTimerclasse che restituisce una serie di attività che si completano con il timer: https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}

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

Semplice...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.