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


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_принцип . Зупиніть таймер на початку OnTick (), перевірте бул, щоб побачити, чи слід щось робити на не, виконуйте роботу, перезапустіть таймер, коли закінчите.
Mike Trusov

Відповіді:


94

Для цього я б використав потік даних 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);
}

Привіт, я намагаюся виконати це, але стикаюся з проблемами. Якщо мій DoWork не приймає аргументів, task = CreateNeverEndingTask (зараз => DoWork (), wtoken.Token); видає мені помилку збірки (невідповідність типу). З іншого боку, якщо мій DoWork приймає параметр DateTimeOffset, той самий рядок видає іншу помилку збірки, повідомляючи, що жодне перевантаження для DoWork не приймає 0 аргументів. Чи не могли б ви допомогти мені це зрозуміти?
Боваз,

1
Власне, я вирішив свою проблему, додавши прив'язку до рядка, де я призначаю завдання, і передаю параметр DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Боваз,

Ви також можете змінити тип "ActionBlock <DateTimeOffset> завдання;" до завдання ITargetBlock <DateTimeOffset>;
XOR

1
Я вважаю, що це, ймовірно, буде виділяти пам’ять назавжди, що в кінцевому результаті призведе до переповнення.
Nate Gardner

@NateGardner У якій частині?
casperOne

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.DelayoverThread.Sleep означає, що ви не зав'язуєте нитку, нічого не роблячи протягом сну.

Якщо у вас є можливість, ви також можете DoWork()прийняти маркер скасування, і скасування буде набагато більш чутливим.


1
Дізнайтеся, яке завдання ви отримаєте, якщо використовувати асинхронну лямбда-параметр як параметр Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Коли ви виконуєте завдання. Зачекайте ( ); після запиту на скасування ви будете чекати неправильного завдання.
Лукас Піркл,

Так, це насправді має бути Task.Run зараз, який має правильне перевантаження.
porges

За словами http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx , схоже , Task.Runвикористовує пул потоків, так що ваш приклад , використовуючи Task.Runзамість того , щоб Task.Factory.StartNewз TaskCreationOptions.LongRunningробить те ж саме - якби мені було потрібне завдання, щоб скористатися цією LongRunningопцією, чи не зміг би я використати, Task.Runяк ви показали, чи щось мені не вистачає?
Джефф

@Lumirris: Суть async / await полягає у тому, щоб уникнути зав'язування потоку протягом усього часу його виконання (тут, під час виклику Delay, завдання не використовує потік). Отже, використання LongRunningє несумісним з метою не пов’язувати нитки. Якщо ви хочете гарантувати запуск на власному потоці, ви можете ним скористатися, але тут ви почнете потік, який більшу частину часу сплячий. Який варіант використання?
porges

@Porges Очко взято. Моїм випадком використання було б завдання, що запускає нескінченний цикл, в якому кожна ітерація виконувала б шматок роботи і `` розслаблялась '' протягом 2 секунд, перш ніж виконувати чергову роботу на наступній ітерації. Він працює вічно, але регулярно роблячи перерви на 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();
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.