Итак, мое приложение должно выполнять действие почти непрерывно (с паузой в 10 секунд или около того между каждым запуском), пока приложение работает или запрашивается отмена. Необходимая работа может занять до 30 секунд.
Не лучше ли использовать System.Timers.Timer и использовать AutoReset, чтобы убедиться, что он не выполняет действие до завершения предыдущего «тика».
Или мне следует использовать общую задачу в режиме LongRunning с токеном отмены и иметь внутри нее обычный бесконечный цикл while, вызывающий действие, выполняющее работу с 10-секундным Thread.Sleep между вызовами? Что касается модели async / await, я не уверен, что она будет здесь уместна, поскольку у меня нет возвращаемых значений из работы.
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.
}
или просто используйте простой таймер при использовании его свойства AutoReset и вызовите .Stop (), чтобы отменить его?
Ответы:
Я бы использовал для этого TPL Dataflow (поскольку вы используете .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
метода расширения):Ваш
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
метод - это фабрика, которая, так сказать, создает вашу «службу». Вы контролируете, когда он запускается и останавливается, и он полностью самодостаточен. Вам не нужно переплетать управление состоянием таймера с другими аспектами вашего кода. Вы просто создаете блок, запускаете его и останавливаете, когда закончите.Более эффективное использование потоков / задач / ресурсов
Планировщик по умолчанию для блоков в потоке данных TPL такой же для a
Task
, который является пулом потоков. ИспользуяActionBlock<TInput>
для обработки вашего действия, а также его вызовTask.Delay
, вы передаете контроль над потоком, который вы использовали, когда на самом деле ничего не делаете. Конечно, это на самом деле приводит к некоторым накладным расходам, когда вы создаете новое,Task
которое будет обрабатывать продолжение, но оно должно быть небольшим, учитывая, что вы не обрабатываете это в жестком цикле (вы ждете десять секунд между вызовами).Если
DoWork
функцию действительно можно сделать ожидаемой (а именно, в том смысле, что она возвращает aTask
), то вы можете (возможно) оптимизировать это еще больше, настроив метод factory выше, чтобы он принимал aFunc<DateTimeOffset, CancellationToken, Task>
вместо anAction<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); }
источник
Я считаю, что новый интерфейс на основе задач очень прост для выполнения подобных действий - даже проще, чем использование класса Timer.
Вы можете внести небольшие изменения в свой пример. Вместо того:
task = Task.Factory.StartNew(() => { while (true) { wtoken.Token.ThrowIfCancellationRequested(); DoWork(); Thread.Sleep(10000); } }, wtoken, TaskCreationOptions.LongRunning);
Ты можешь это сделать:
task = Task.Run(async () => // <- marked async { while (true) { DoWork(); await Task.Delay(10000, wtoken.Token); // <- await with cancellation } }, wtoken.Token);
Таким образом, отмена будет происходить мгновенно, если она находится внутри
Task.Delay
, вместо того, чтобы ждатьThread.Sleep
завершения.Кроме того, использование
Task.Delay
overThread.Sleep
означает, что вы не привязываете поток, ничего не делая во время сна.Если у вас есть возможность, вы также можете заставить
DoWork()
принять токен отмены, и отмена будет намного быстрее.источник
Task.Run
используется пул потоков, поэтому ваш пример, использующийTask.Run
вместоTask.Factory.StartNew
withTaskCreationOptions.LongRunning
, не делает то же самое - если мне нужна задача для использования этойLongRunning
опции, я не смогу использовать,Task.Run
как вы показали, или мне что-то не хватает?LongRunning
несовместимо с целью не связывать потоки. Если вы хотите гарантировать работу в собственном потоке, вы можете использовать его, но здесь вы собираетесь запустить поток, который большую часть времени спит. Какой вариант использования?LongRunning
с помощьюTask.Run
синтаксиса. Из документации кажется, чтоTask.Run
это более чистый синтаксис, если вас устраивают используемые по умолчанию настройки. Кажется, что у него нет перегрузки, требующейTaskCreationOptions
аргумента.Вот что я придумал:
NeverEndingTask
и замените егоExecutionCore
той работой, которую вы хотите выполнить.ExecutionLoopDelayMs
позволяет вам регулировать время между циклами, например, если вы хотите использовать алгоритм отсрочки.Start/Stop
предоставить синхронный интерфейс для запуска / остановки задачи.LongRunning
означает, что вы получите один выделенный поток на каждыйNeverEndingTask
.ActionBlock
решения на основе выше.:
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(); } }
источник