Eu usaria o TPL Dataflow para isso (já que você está usando o .NET 4.5 e ele usa Task
internamente). 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 DateTimeOffset
estrutura ; 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 Post
método de extensão de volta em si).
Também passei a CancellationToken
estrutura para o construtor de ActionBlock<TInput>
e para a chamada do Task.Delay
mé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 Post
método de extensão):
CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;
Seu StartWork
mé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 StopWork
mé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 CreateNeverEndingTask
mé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 Task
que 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 DoWork
funçã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 CancellationToken
o método em seu método (se ele aceitar), o que é feito aqui.
Isso significa que você teria um DoWorkAsync
mé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 StartWork
método para contabilizar a nova assinatura passada para o CreateNeverEndingTask
mé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);
}