Ответ 1
Я использовал бы TPL Dataflow для этого (поскольку вы используете .NET 4.5 и использует Task
внутренне). Вы можете легко создать ActionBlock<TInput>
, который отправляет элементы себе после того, как обработал это действие и подождал подходящее количество времени.
Сначала создайте factory, который создаст вашу бесконечную задачу:
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;
}
Я выбрал ActionBlock<TInput>
для создания структуры DateTimeOffset
; вам нужно передать параметр типа, и он может также передать некоторое полезное состояние (вы можете изменить характер состояния, если хотите).
Также обратите внимание, что ActionBlock<TInput>
по умолчанию обрабатывает только один элемент за раз, поэтому вам гарантируется, что будет обработано только одно действие (что означает, что вам не придется иметь дело с reentrancy, когда он вызывает метод Post
сам по себе).
Я также передал структуру CancellationToken
как для конструктора ActionBlock<TInput>
, так и для Task.Delay
метод call; если процесс отменен, отмена будет выполнена при первой возможной возможности.
Оттуда, это простой рефакторинг вашего кода для хранения ITargetBlock<DateTimeoffset>
интерфейса, реализованного ActionBlock<TInput>
(это более высокий, абстракция уровня, представляющая блоки, являющиеся потребителями, и вы хотите, чтобы иметь возможность запускать потребление посредством вызова метода расширения Post
):
CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;
Ваш метод StartWork
:
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);
}
И затем ваш метод StopWork
:
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;
}
Почему вы хотите использовать TPL Dataflow здесь? Несколько причин:
Разделение проблем
Теперь метод CreateNeverEndingTask
представляет собой factory, который создает вашу "службу", так сказать. Вы управляете, когда оно начинается и останавливается, и оно полностью самодостаточно. Вам не нужно переплетать контроль состояния таймера с другими аспектами вашего кода. Вы просто создаете блок, запускаете его и останавливаете, когда закончите.
Более эффективное использование потоков/задач/ресурсов
Планировщик по умолчанию для блоков в потоке данных TPL одинаковый для Task
, который является пулом потоков. Используя ActionBlock<TInput>
для обработки вашего действия, а также вызов Task.Delay
, вы получаете контроль над потоком, который вы использовали, когда вы на самом деле ничего не делаете. Конечно, это приводит к некоторым накладным расходам, когда вы создаете новый Task
, который будет обрабатывать продолжение, но это должно быть небольшим, учитывая, что вы не обрабатываете это в узком цикле (вы ожидаете десять секунд между вызовами).
Если функция DoWork
действительно может быть сделана ожидаемой (а именно, что она возвращает Task
), то вы можете (возможно) оптимизировать ее еще больше, изменив метод factory выше, чтобы взять Func<DateTimeOffset, CancellationToken, Task>
вместо Action<DateTimeOffset>
, например:
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;
}
Конечно, было бы хорошей практикой сплести CancellationToken
с помощью вашего метода (если он примет его), который делается здесь.
Это означает, что у вас будет метод DoWorkAsync
со следующей сигнатурой:
Task DoWorkAsync(CancellationToken cancellationToken);
Вам нужно было бы изменить (только немного, и вы не отключаете разделение проблем) метод StartWork
для учета новой сигнатуры, переданной методу CreateNeverEndingTask
, например:
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);
}