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

92

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

Джош
источник
Задача кажется излишней, учитывая то, чего вы пытаетесь достичь. en.wikipedia.org/wiki/KISS_principle . Остановите таймер при запуске OnTick (), проверьте логическое значение, чтобы узнать, нужно ли вам что-либо делать, не выполняйте работу, перезапустите таймер, когда закончите.
Майк Трусов

Ответы:

94

Я бы использовал для этого 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метода расширения):

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метод - это фабрика, которая, так сказать, создает вашу «службу». Вы контролируете, когда он запускается и останавливается, и он полностью самодостаточен. Вам не нужно переплетать управление состоянием таймера с другими аспектами вашего кода. Вы просто создаете блок, запускаете его и останавливаете, когда закончите.

Более эффективное использование потоков / задач / ресурсов

Планировщик по умолчанию для блоков в потоке данных TPL такой же для a Task, который является пулом потоков. Используя ActionBlock<TInput>для обработки вашего действия, а также его вызов Task.Delay, вы передаете контроль над потоком, который вы использовали, когда на самом деле ничего не делаете. Конечно, это на самом деле приводит к некоторым накладным расходам, когда вы создаете новое, Taskкоторое будет обрабатывать продолжение, но оно должно быть небольшим, учитывая, что вы не обрабатываете это в жестком цикле (вы ждете десять секунд между вызовами).

Если DoWorkфункцию действительно можно сделать ожидаемой (а именно, в том смысле, что она возвращает a Task), то вы можете (возможно) оптимизировать это еще больше, настроив метод factory выше, чтобы он принимал a 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);
}
casperOne
источник
Здравствуйте, я пытаюсь реализовать эту реализацию, но у меня возникают проблемы. Если мой DoWork не принимает аргументов, task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); дает мне ошибку сборки (несоответствие типа). С другой стороны, если мой DoWork принимает параметр DateTimeOffset, эта же строка дает мне другую ошибку сборки, говоря мне, что никакая перегрузка для DoWork не принимает 0 аргументов. Не могли бы вы помочь мне разобраться в этом?
Bovaz
1
Собственно, свою проблему я решил, добавив приведение к строке, где я назначаю задачу, и передав параметр в DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz
Вы также могли изменить тип «ActionBlock <DateTimeOffset> task;» в задачу ITargetBlock <DateTimeOffset>;
XOR
1
Я считаю, что это, вероятно, навсегда выделит память, что в конечном итоге приведет к переполнению.
Нейт Гарднер
@NateGardner В какой части?
casperOne 08
75

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

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

каши
источник
1
Какую задачу вы получите, если вы используете асинхронную лямбду в качестве параметра Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Когда вы выполняете task.Wait ( ); после запроса на отмену вы будете ждать некорректной задачи.
Лукас Пиркл
Да, на самом деле это должен быть Task.Run now с правильной перегрузкой.
porges
Согласно http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx, похоже, что Task.Runиспользуется пул потоков, поэтому ваш пример, использующий Task.Runвместо Task.Factory.StartNewwith TaskCreationOptions.LongRunning, не делает то же самое - если мне нужна задача для использования этой LongRunningопции, я не смогу использовать, Task.Runкак вы показали, или мне что-то не хватает?
Джефф
@Lumirris: суть async / await в том, чтобы не связывать поток на все время его выполнения (здесь, во время вызова задержки, задача не использует поток). Таким образом, использование LongRunningнесовместимо с целью не связывать потоки. Если вы хотите гарантировать работу в собственном потоке, вы можете использовать его, но здесь вы собираетесь запустить поток, который большую часть времени спит. Какой вариант использования?
porges
@Porges Точка взята. Моим вариантом использования была бы задача, выполняющая бесконечный цикл, в котором каждая итерация выполняла бы часть работы и «расслаблялась» в течение 2 секунд, прежде чем выполнять еще одну часть работы на следующей итерации. Он работает вечно, но с регулярными двухсекундными перерывами. Мой комментарий, однако, был больше о том, можно ли указать это LongRunningс помощью Task.Runсинтаксиса. Из документации кажется, что Task.Runэто более чистый синтаксис, если вас устраивают используемые по умолчанию настройки. Кажется, что у него нет перегрузки, требующей TaskCreationOptionsаргумента.
Джефф
4

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

  • Наследуйте метод 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();
    }
}
Джек Уклея
источник