Maneira adequada de implementar uma tarefa sem fim. (Timers vs Tarefa)

92

Portanto, meu aplicativo precisa executar uma ação quase continuamente (com uma pausa de 10 segundos ou mais entre cada execução) enquanto o aplicativo estiver em execução ou um cancelamento for solicitado. O trabalho que ela precisa fazer pode levar até 30 segundos.

É melhor usar um System.Timers.Timer e usar o AutoReset para garantir que ele não execute a ação antes que o "tique" anterior seja concluído.

Ou devo usar uma tarefa geral no modo LongRunning com um token de cancelamento e ter um loop while infinito regular dentro dele chamando a ação que faz o trabalho com um Thread.Sleep de 10 segundos entre as chamadas? Quanto ao modelo async / await, não tenho certeza se seria apropriado aqui, pois não tenho nenhum valor de retorno do trabalho.

CancellationTokenSource wtoken;
Task task;

void StopWork()
{
    wtoken.Cancel();

    try 
    {
        task.Wait();
    } catch(AggregateException) { }
}

void StartWork()
{
    wtoken = new CancellationTokenSource();

    task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            wtoken.Token.ThrowIfCancellationRequested();
            DoWork();
            Thread.Sleep(10000);
        }
    }, wtoken, TaskCreationOptions.LongRunning);
}

void DoWork()
{
    // Some work that takes up to 30 seconds but isn't returning anything.
}

ou apenas usar um temporizador simples enquanto usa sua propriedade AutoReset e chamar .Stop () para cancelá-lo?

Josh
fonte
A tarefa parece um exagero, considerando o que você está tentando alcançar. en.wikipedia.org/wiki/KISS_principle . Pare o cronômetro no início de OnTick (), verifique um bool para ver se você deve estar fazendo algo em não, trabalhar e reiniciar o cronômetro quando terminar.
Mike Trusov

Respostas:

94

Eu usaria o TPL Dataflow para isso (já que você está usando o .NET 4.5 e ele usa Taskinternamente). Você pode criar facilmente um ActionBlock<TInput>que posta itens para si mesmo depois de processado sua ação e aguardado um período de tempo apropriado.

Primeiro, crie uma fábrica que criará sua tarefa sem fim:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.
        action(now);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

Eu escolhi o ActionBlock<TInput>para ter uma DateTimeOffsetestrutura ; você tem que passar um parâmetro de tipo, e também pode passar algum estado útil (você pode alterar a natureza do estado, se quiser).

Além disso, observe que, ActionBlock<TInput>por padrão, processa apenas um item por vez, então você tem a garantia de que apenas uma ação será processada (ou seja, você não terá que lidar com a reentrada quando ele chamar o Postmétodo de extensão de volta em si).

Também passei a CancellationTokenestrutura para o construtor de ActionBlock<TInput>e para a chamada do Task.Delaymétodo ; se o processo for cancelado, o cancelamento ocorrerá na primeira oportunidade possível.

A partir daí, é uma refatoração fácil de seu código para armazenar a ITargetBlock<DateTimeoffset>interface implementada ActionBlock<TInput>(esta é a abstração de nível superior que representa os blocos que são consumidores e você deseja ser capaz de acionar o consumo por meio de uma chamada ao Postmétodo de extensão):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Seu StartWorkmétodo:

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now);
}

E então seu StopWorkmétodo:

void StopWork()
{
    // CancellationTokenSource implements IDisposable.
    using (wtoken)
    {
        // Cancel.  This will cancel the task.
        wtoken.Cancel();
    }

    // Set everything to null, since the references
    // are on the class level and keeping them around
    // is holding onto invalid state.
    wtoken = null;
    task = null;
}

Por que você deseja usar o TPL Dataflow aqui? Alguns motivos:

Separação de preocupações

O CreateNeverEndingTaskmétodo agora é uma fábrica que cria seu "serviço", por assim dizer. Você controla quando ele começa e para, e é totalmente independente. Você não precisa entrelaçar o controle de estado do cronômetro com outros aspectos do seu código. Você simplesmente cria o bloco, inicia-o e interrompe-o quando terminar.

Uso mais eficiente de threads / tarefas / recursos

O agendador padrão para os blocos no fluxo de dados TPL é o mesmo para a Task, que é o pool de threads. Ao usar o ActionBlock<TInput>para processar sua ação, bem como uma chamada para Task.Delay, você está cedendo o controle do encadeamento que estava usando quando, na verdade, não estava fazendo nada. Concedido, isso realmente leva a alguma sobrecarga quando você cria o novo Taskque processará a continuação, mas isso deve ser pequeno, considerando que você não está processando isso em um loop apertado (você está esperando dez segundos entre as invocações).

Se a DoWorkfunção realmente pode ser tornada aguardável (ou seja, em que retorna a Task), então você pode (possivelmente) otimizar isso ainda mais ajustando o método de fábrica acima para obter um em Func<DateTimeOffset, CancellationToken, Task>vez de um Action<DateTimeOffset>, como:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.  Wait on the result.
        await action(now, cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Same as above.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

Obviamente, seria uma boa prática incluir CancellationTokeno método em seu método (se ele aceitar), o que é feito aqui.

Isso significa que você teria um DoWorkAsyncmétodo com a seguinte assinatura:

Task DoWorkAsync(CancellationToken cancellationToken);

Você teria que mudar (apenas ligeiramente, e você não está eliminando a separação de preocupações aqui) o StartWorkmétodo para contabilizar a nova assinatura passada para o CreateNeverEndingTaskmétodo, assim:

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now, wtoken.Token);
}
casperOne
fonte
Olá, estou tentando esta implementação, mas estou enfrentando problemas. Se meu DoWork não aceita nenhum argumento, task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); me dá um erro de compilação (incompatibilidade de tipo). Por outro lado, se meu DoWork leva um parâmetro DateTimeOffset, essa mesma linha me dá um erro de compilação diferente, me informando que nenhuma sobrecarga para DoWork leva 0 argumentos. Você poderia me ajudar a descobrir isso?
Bovaz
1
Na verdade, resolvi meu problema adicionando um elenco à linha onde atribuo a tarefa e passando o parâmetro para DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz
Você também pode ter alterado o tipo de "ActionBlock <DateTimeOffset> tarefa;" para a tarefa ITargetBlock <DateTimeOffset>;
XOR
1
Acredito que isso provavelmente alocará memória para sempre, levando a um estouro.
Nate Gardner
@NateGardner Em que parte?
casperOne
75

Acho que a nova interface baseada em tarefas é muito simples para fazer coisas como esta - ainda mais fácil do que usar a classe Timer.

Existem alguns pequenos ajustes que você pode fazer em seu exemplo. Ao invés de:

task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        wtoken.Token.ThrowIfCancellationRequested();
        DoWork();
        Thread.Sleep(10000);
    }
}, wtoken, TaskCreationOptions.LongRunning);

Você consegue fazer isso:

task = Task.Run(async () =>  // <- marked async
{
    while (true)
    {
        DoWork();
        await Task.Delay(10000, wtoken.Token); // <- await with cancellation
    }
}, wtoken.Token);

Desta forma, o cancelamento acontecerá instantaneamente se dentro do Task.Delay, ao invés de ter que esperar peloThread.Sleep término do.

Além disso, usando Task.DelaymaisThread.Sleep significa que você não está amarrando uma linha sem fazer nada durante o sono.

Se puder, você também pode DoWork()aceitar um token de cancelamento, e o cancelamento será muito mais ágil.

porges
fonte
1
Descubra qual tarefa você obterá se usar o lambda assíncrono como parâmetro de Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Quando você faz task.Wait ( ); após o cancelamento ser solicitado, você estará aguardando a tarefa incorreta.
Lukas Pirkl
Sim, deve ser Task.Run now, que tem a sobrecarga correta.
porges de
De acordo com http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx , parece que Task.Runusa o pool de threads, então seu exemplo usando em Task.Runvez de Task.Factory.StartNewcom TaskCreationOptions.LongRunningnão faz exatamente a mesma coisa - se eu precisasse da tarefa para usar a LongRunningopção, não seria capaz de usar Task.Runcomo você mostrou ou estou faltando alguma coisa?
Jeff
@Lumirris: O ponto de async / await é evitar amarrar um thread durante todo o tempo em que está sendo executado (aqui, durante a chamada Delay, a tarefa não está usando um thread). Portanto, o uso LongRunningé incompatível com o objetivo de não amarrar os fios. Se você quiser garantir a execução em seu próprio encadeamento, poderá usá-lo, mas aqui você iniciará um encadeamento que está inativo a maior parte do tempo. Qual é o caso de uso?
porges de
@Porges Ponto tomado. Meu caso de uso seria uma tarefa executando um loop infinito, no qual cada iteração faria uma parte do trabalho e 'relaxaria' por 2 segundos antes de fazer outra parte do trabalho na próxima iteração. Está funcionando para sempre, mas com intervalos regulares de 2 segundos. Meu comentário, porém, era mais sobre se você poderia especificá-lo LongRunningusando a Task.Runsintaxe. Pela documentação, parece uma Task.Runsintaxe mais limpa, contanto que você esteja satisfeito com as configurações padrão que ela usa. Não parece haver uma sobrecarga que exige um TaskCreationOptionsargumento.
Jeff,
4

Aqui está o que eu descobri:

  • Herdar NeverEndingTaske sobrescrever o ExecutionCoremétodo com o trabalho que você deseja fazer.
  • A mudança ExecutionLoopDelayMspermite que você ajuste o tempo entre os loops, por exemplo, se você quiser usar um algoritmo de backoff.
  • Start/Stop fornecer uma interface síncrona para iniciar / parar a tarefa.
  • LongRunningsignifica que você receberá um tópico dedicado por NeverEndingTask.
  • Esta classe não aloca memória em um loop, ao contrário da ActionBlocksolução baseada acima.
  • O código abaixo é um esboço, não necessariamente um código de produção :)

:

public abstract class NeverEndingTask
{
    // Using a CTS allows NeverEndingTask to "cancel itself"
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    protected NeverEndingTask()
    {
         TheNeverEndingTask = new Task(
            () =>
            {
                // Wait to see if we get cancelled...
                while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs))
                {
                    // Otherwise execute our code...
                    ExecutionCore(_cts.Token);
                }
                // If we were cancelled, use the idiomatic way to terminate task
                _cts.Token.ThrowIfCancellationRequested();
            },
            _cts.Token,
            TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning);

        // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable
        TheNeverEndingTask.ContinueWith(x =>
        {
            Trace.TraceError(x.Exception.InnerException.Message);
            // Log/Fire Events etc.
        }, TaskContinuationOptions.OnlyOnFaulted);

    }

    protected readonly int ExecutionLoopDelayMs = 0;
    protected Task TheNeverEndingTask;

    public void Start()
    {
       // Should throw if you try to start twice...
       TheNeverEndingTask.Start();
    }

    protected abstract void ExecutionCore(CancellationToken cancellationToken);

    public void Stop()
    {
        // This code should be reentrant...
        _cts.Cancel();
        TheNeverEndingTask.Wait();
    }
}
Jack Ukleja
fonte