Правильный способ реализации бесконечной задачи. (Таймеры против задачи)

Итак, моему приложению необходимо выполнить действие почти непрерывно (с паузой в 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(), чтобы отменить его?

Ответ 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);
}

Ответ 2

Я считаю, что новый интерфейс на основе задач очень прост для выполнения подобных действий - даже проще, чем использование класса 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 over Thread.Sleep означает, что вы не связываете нить, ничего не делающую в течение всего времени сна.

Если вы в состоянии, вы также можете сделать DoWork() принять токен отмены, и отмена будет гораздо более отзывчивой.

Ответ 3

Вот что я придумал:

  • Наследовать от 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();
    }
}