Existe uma substituição baseada em tarefas para System.Threading.Timer?

88

Sou novo no Tasks do .Net 4.0 e não consegui encontrar o que achei que seria uma substituição baseada em tarefas ou implementação de um temporizador, por exemplo, uma tarefa periódica. Existe uma coisa dessas?

Atualização Eu vim com o que penso ser uma solução para minhas necessidades que é envolver a funcionalidade "Timer" dentro de uma Tarefa com tarefas filhas, todas aproveitando o CancelamentoToken e retornando a Tarefa para poder participar de outras etapas da Tarefa.

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
fonte
7
Você deve usar um Timer dentro da Tarefa em vez de usar o mecanismo Thread.Sleep. É mais eficiente.
Yoann. B

Respostas:

84

Depende do 4.5, mas 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, você pode adicionar uma versão genérica que também aceita argumentos. Na verdade, isso é semelhante a outras abordagens sugeridas, já que o Task.Delay está usando uma expiração de cronômetro como uma fonte de conclusão de tarefa.

Jeff
fonte
1
Mudei para essa abordagem agora. Mas eu condicionalmente chamo action()com uma repetição de !cancelToken.IsCancellationRequested. Assim está melhor, certo?
HappyNomad
3
Obrigado por isso - estamos usando o mesmo, mas mudamos o atraso para depois da ação (faz mais sentido para nós, pois precisamos chamar a ação imediatamente e repetir depois de x)
Michael Parker
1
Obrigado por isso. Mas este código não será executado "a cada X horas", ele será executado "a cada X horas + tempo de actionexecução", estou certo?
Alex
Corrigir. Você precisaria de matemática se quiser contabilizar o tempo de execução. No entanto, isso pode ser complicado se o tempo de execução exceder o seu período, etc ...
Jeff
57

ATUALIZAÇÃO Estou marcando a resposta abaixo como a "resposta", pois ela já é antiga o suficiente para que possamos usar o padrão async / await. Não há necessidade de downvote isso mais. ri muito


Como Amy respondeu, não há implementação periódica / temporizada baseada em tarefas. No entanto, com base no meu UPDATE original, evoluímos para algo bastante útil e testado em produção. Pensei em compartilhar:

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

Resultado:

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
fonte
1
Parece um ótimo código, mas estou me perguntando se é necessário agora que existem as palavras-chave async / await. Como sua abordagem se compara a esta aqui: stackoverflow.com/a/14297203/122781 ?
HappyNomad
1
@HappyNomad, parece que a classe PeriodicTaskFactory poderia aproveitar async / await para aplicativos que visam .Net 4.5, mas para nós, não podemos mudar para .Net 4.5 ainda. Além disso, a PeriodicTaskFactory fornece alguns mecanismos adicionais de finalização de "cronômetro", como número máximo de iterações e duração máxima, além de fornecer uma maneira de garantir que cada iteração possa aguardar a última iteração. Mas tentarei adaptar isso para usar assíncrono / esperar quando mudarmos para .Net 4.5
Jim
4
+1 Estou usando sua classe agora, obrigado. Para fazê-lo funcionar bem com o thread de interface do usuário, porém, tenho que chamar TaskScheduler.FromCurrentSynchronizationContext()antes de configurar mainAction. Em seguida, passo o agendador resultante MainPeriodicTaskActionpara criar o subTaskcom.
HappyNomad
2
Não tenho certeza, é uma boa ideia bloquear um tópico, quando ele pode fazer um trabalho útil. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Então você usa um Timer, você espera no hardware, então nenhum thread é gasto. Mas em sua solução, os threads são gastos para nada.
RollingStone
2
@rollingstone eu concordo. Acho que essa solução derrota em grande parte o propósito do comportamento assíncrono. Muito melhor usar um cronômetro e não desperdiçar o fio. Isso é apenas dar a aparência de assíncrono, sem nenhum dos benefícios.
Jeff,
12

Não está exatamente no System.Threading.Tasks, mas Observable.Timer(ou mais simples Observable.Interval) da biblioteca Reactive Extensions é provavelmente o que você está procurando.

mstone
fonte
1
Ex: Observable.Interval (TimeSpan.FromSeconds (1)). Assinar (v => Debug.WriteLine (v));
Martin Capodici
1
Legal, mas esses constructos reativos são canalizáveis?
Shmil The Cat
9

Até agora eu usei uma tarefa TPL LongRunning para trabalho em segundo plano cíclico de CPU em vez do temporizador de threading, porque:

  • a tarefa TPL suporta cancelamento
  • o temporizador de threading pode iniciar outro thread enquanto o programa está sendo encerrado, causando possíveis problemas com recursos descartados
  • chance de saturação: o cronômetro de threading pode iniciar outro encadeamento enquanto o anterior ainda está sendo processado devido a um longo trabalho inesperado (eu sei, pode ser evitado parando e reiniciando o cronômetro)

No entanto, a solução TPL sempre reivindica um thread dedicado que não é necessário enquanto espera pela próxima ação (que é na maioria das vezes). Eu gostaria de usar a solução proposta de Jeff para realizar trabalho cíclico vinculado à CPU em segundo plano, porque ela só precisa de um thread de pool de threads quando há trabalho a ser feito, o que é melhor para escalabilidade (especialmente quando o período de intervalo é grande).

Para conseguir isso, gostaria de sugerir 4 adaptações:

  1. Adicione ConfigureAwait(false)ao Task.Delay()para executar a doWorkação em um thread do pool de threads, caso contráriodoWork será realizada na thread de chamada, que não é a ideia de paralelismo
  2. Atenha-se ao padrão de cancelamento lançando uma TaskCanceledException (ainda necessária?)
  3. Encaminhe o CancelamentoToken para doWork para habilitá-lo a cancelar a tarefa
  4. Adicione um parâmetro do tipo objeto para fornecer informações de estado da tarefa (como uma tarefa TPL)

Sobre o ponto 2, não tenho certeza, o async await ainda requer a TaskCanceledExecption ou é apenas uma prática recomendada?

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

Dê seus comentários sobre a solução proposta ...

Atualização 2016-8-30

A solução acima não chama imediatamente, doWork()mas começa com await Task.Delay().ConfigureAwait(false)para alcançar a troca de thread para doWork(). A solução abaixo supera esse problema envolvendo a primeira doWork()chamada em a Task.Run()e aguardando-a.

Abaixo está a substituição async \ await aprimorada para Threading.Timerque executa trabalho cíclico cancelável e é escalonável (em comparação com a solução TPL) porque não ocupa nenhum thread enquanto espera pela próxima ação.

Note que ao contrário do Timer, o tempo de espera ( period) é constante e não o tempo do ciclo; o tempo de ciclo é a soma do tempo de espera e cuja duração doWork()pode 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
fonte
Usar ConfigureAwait(false)irá agendar a continuação do método para o pool de threads, então realmente não resolve o segundo ponto referente ao timer de threading. Eu também não acho que taskStateseja necessário; A captura da variável lambda é mais flexível e segura para o tipo.
Stephen Cleary
1
O que eu realmente quero fazer é trocar await Task.Delay()e doWork()assim doWork()seria imediatamente executado durante a inicialização. Mas, sem algum truque, doWork()ele executaria no thread de chamada pela primeira vez e o bloquearia. Stephen, você tem uma solução para esse problema?
Erik Stroeken
1
A maneira mais fácil é envolver tudo em um Task.Run.
Stephen Cleary
Sim, mas posso simplesmente voltar para a solução TPL que uso agora, que reivindica um thread enquanto o loop estiver em execução e, portanto, é menos escalonável do que esta solução.
Erik Stroeken
1

Eu precisava acionar as tarefas assíncronas recorrentes de um método síncrono.

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 é uma adaptação da resposta de Jeff. Ele é alterado para incluir um. Func<Task> Ele também garante que o período seja a frequência com que ela é executada, deduzindo o tempo de execução da tarefa do período para o próximo atraso.

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

Eu tive um problema semelhante e escrevi uma TaskTimerclasse que retorna uma série de tarefas que são concluídas no cronômetro: 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
fonte
-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; }
}

Simples...

nim
fonte