Для цього я б використав потік даних TPL (оскільки ви використовуєте .NET 4.5, а він використовує Task
внутрішньо). Ви можете легко створити елемент, ActionBlock<TInput>
який публікує елементи для себе, після того, як він буде оброблений і буде чекати відповідну кількість часу.
Спочатку створіть фабрику, яка створить ваше нескінченне завдання:
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>
за замовчуванням за один раз обробляється лише один елемент, тож ви гарантовано обробляєте лише одну дію (тобто, вам не доведеться мати справу з повторним поверненням, коли він знову викликає Post
метод розширення ).
Я також передав CancellationToken
структуру як конструктору, так ActionBlock<TInput>
і виклику Task.Delay
методу ; якщо процес скасовано, скасування відбудеться при першій можливості.
Звідти це легка рефакторинг вашого коду для зберігання 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? Кілька причин:
Поділ проблем
Зараз CreateNeverEndingTask
метод - це фабрика, яка створює вашу «послугу», так би мовити. Ви контролюєте, коли він запускається і зупиняється, і він повністю автономний. Вам не потрібно переплітати державний контроль таймера з іншими аспектами вашого коду. Ви просто створюєте блок, запускаєте його і зупиняєте, коли закінчите.
Більш ефективне використання потоків / завдань / ресурсів
Планувальник за замовчуванням для блоків у потоці даних TPL однаковий для a Task
, який є пулом потоків. Використовуючи ActionBlock<TInput>
для обробки вашої дії, а також заклик до Task.Delay
, ви отримуєте контроль над потоком, який ви використовували, коли насправді нічого не робите. Звичайно, це насправді призводить до певних накладних витрат, коли ви створюєте нове, Task
яке обробляє продовження, але це має бути невеликим, враховуючи, що ви не обробляєте це в щільному циклі (ви чекаєте десять секунд між викликами).
Якщо DoWork
функцію насправді можна зробити очікуваною (а саме тим, що вона повертає a Task
), тоді ви можете (можливо) оптимізувати це ще більше, налаштувавши заводський метод вище, щоб Func<DateTimeOffset, CancellationToken, Task>
замість an взяти 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);
}