Чи існує заміна завдання на основі System.Threading.Timer?


91

Я новачок у завданнях .Net 4.0, і я не зміг знайти те, що, на мою думку, було б заміною на основі завдань або реалізацією таймера, наприклад періодичне завдання. Чи є така річ?

Оновлення Я придумав рішення, яке, на мою думку, є рішенням для моїх потреб, а саме: обернути функцію "Таймер" всередині Завдання з дочірніми Завданнями, усі користуючись перевагами CancellationToken, і повернути Завдання, щоб мати можливість брати участь у подальших кроках Завдання.

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      

7
Вам слід використовувати таймер всередині Завдання, а не використовувати механізм Thread.Sleep. Це ефективніше.
Йоанн. B

Відповіді:


85

Це залежить від 4,5, але це працює.

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

Очевидно, ви можете додати загальну версію, яка також бере аргументи. Це насправді схоже на інші запропоновані підходи, оскільки під капотом Task.Delay використовує термін дії таймера як джерело виконання завдання.


1
Я перейшов на такий підхід щойно. Але я умовно дзвоню action()з повторенням !cancelToken.IsCancellationRequested. Так краще, правда?
HappyNomad

3
Дякуємо за це - ми використовуємо те саме, але перенесли затримку до закінчення дії (для нас це має більший сенс, оскільки нам потрібно негайно викликати дію, а потім повторити після x)
Майкл Паркер,

2
Дякую за це. Але цей код не буде працювати "кожні X години", він буде працювати "кожні X години + час actionвиконання", я маю рацію?
Олексій

Правильно. Вам знадобиться трохи математики, якщо ви хочете врахувати час виконання. Однак це може бути складно, якщо час виконання перевищує ваш період тощо ...
Джефф

57

ОНОВЛЕННЯ Я позначаю відповідь нижче як "відповідь", оскільки вона вже досить стара, тому ми повинні використовувати шаблон async / await. Не потрібно більше голосувати проти цього. Лол


Як відповіла Емі, немає періодичної / таймерної реалізації на основі завдань. Однак, базуючись на моєму оригінальному ОНОВЛЕННІ, ми перетворили це на щось цілком корисне та випробуване виробництвом. Думав, я поділюсь:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

Вихід:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .

1
Це виглядає як чудовий код, але мені цікаво, чи потрібно це зараз, коли є ключові слова async / await. Як ваш підхід порівнюється з наведеним тут: stackoverflow.com/a/14297203/122781 ?
HappyNomad

1
@HappyNomad, схоже, клас PeriodicTaskFactory може скористатися перевагами async / await для програм, націлених на .Net 4.5, але для нас ми поки що не можемо перейти на .Net 4.5. Крім того, PeriodicTaskFactory забезпечує деякі додаткові механізми припинення "таймера", такі як максимальна кількість ітерацій та максимальна тривалість, а також забезпечує спосіб забезпечити, щоб кожна ітерація могла зачекати на останній ітерації. Але я буду прагнути адаптувати це для використання async / await, коли ми перейдемо до .Net 4.5
Джим,

4
+1 Я зараз використовую ваш клас, дякую. Однак, щоб він добре грав з потоком інтерфейсу користувача, мені потрібно зателефонувати TaskScheduler.FromCurrentSynchronizationContext()перед налаштуванням mainAction. Потім я передаю отриманий планувальник, MainPeriodicTaskActionщоб він створив subTaskс.
HappyNomad

2
Не впевнений, це гарна ідея заблокувати потік, коли це може зробити корисну роботу. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Потім ви використовуєте таймер, ви чекаєте в апаратному забезпеченні, тому потоки не витрачаються. Але у вашому рішенні нитки витрачаються даремно.
RollingStone

2
@rollingstone Я згоден. Я думаю, що це рішення значною мірою перемагає мету асинхронної поведінки. Набагато краще використовувати таймер і не витрачати нитку. Це просто надає видимість асинхронізації без будь-яких переваг.
Джефф

12

Це не зовсім так System.Threading.Tasks, але Observable.Timer(або простіше Observable.Interval) з бібліотеки Reactive Extensions - це, мабуть, те, що ви шукаєте.


1
Наприклад, Observable.Interval (TimeSpan.FromSeconds (1)). Підписатися (v => Debug.WriteLine (v));
Мартін Каподічі

1
Приємно, але чи є ці реактивні конструкції відмінні?
Кот

9

Дотепер я використовував завдання LongRunning TPL для циклічної роботи з фоновим процесором замість таймера потоків, оскільки:

  • завдання TPL підтримує скасування
  • таймер потоків може запустити інший потік, поки програма вимикається, що спричиняє можливі проблеми з розпорядженими ресурсами
  • шанс перевиконання: таймер потоків може запустити інший потік, поки попередній все ще обробляється через несподівану тривалу роботу (я знаю, це можна запобігти зупинкою та перезапуском таймера)

Однак рішення TPL завжди вимагає виділеного потоку, який не є необхідним під час очікування наступної дії (а це більшу частину часу). Я хотів би використати запропоноване рішення Джеффа для виконання циклічної роботи, пов'язаної з процесором, у фоновому режимі, оскільки йому потрібен потік пулу потоків лише тоді, коли потрібно виконати роботу, яка є кращою для масштабованості (особливо, коли інтервальний період великий).

Для досягнення цього я б запропонував 4 адаптації:

  1. Додайте ConfigureAwait(false)до, Task.Delay()щоб виконати doWorkдію з потоком пулу потоків, інакшеdoWork буде виконано з викличним потоком, що не є ідеєю паралелізму
  2. Дотримуйтесь шаблону скасування, кинувши TaskCanceledException (все ще потрібно?)
  3. Переслати Скасування, прийняте до doWork дозволити йому скасувати завдання
  4. Додайте параметр об’єкта типу для подання інформації про стан завдання (наприклад, завдання TPL)

Щодо пункту 2, я не впевнений, чи вимагає асинхронізація все-таки TaskCanceledExecption, чи це просто найкраща практика?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

Будь ласка, дайте свої коментарі до запропонованого рішення ...

Оновлення 2016-8-30

Вищевказане рішення не одразу викликає, doWork()а починається з await Task.Delay().ConfigureAwait(false)досягнення перемикача потоку для doWork(). Рішення, наведене нижче, долає цю проблему, обертаючи перший doWork()виклик у Task.Run()і чекаючи його.

Нижче наведено вдосконалений асинхронний файл \ await заміна, Threading.Timerякий виконує циклічну роботу, що скасовується, і є масштабованим (порівняно з рішенням TPL), оскільки він не займає жодного потоку в очікуванні наступної дії.

Зверніть увагу, що, на відміну від таймера, час очікування ( period) постійний, а не час циклу; час циклу - це сума часу очікування, тривалість doWork()якого може змінюватися.

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

Використання ConfigureAwait(false)планує продовження методу до пулу потоків, тому насправді не вирішує другий момент за таймером потоків. Я також не вважаю taskStateнеобхідним; Захоплення лямбда-змінної є більш гнучким і безпечним для типу.
Стівен Клірі

1
Що я дійсно хочу зробити, це обмінятись, await Task.Delay()і doWork()це doWork()негайно виконати під час запуску. Але без певної хитрості doWork()перший раз буде виконано викличний потік і заблоковано. Стівене, чи маєш ти рішення цієї проблеми?
Ерік Штрокен

1
Найпростіший спосіб - просто обернути все це в Task.Run.
Стівен Клірі

Так, але тоді я можу просто повернутися до рішення TPL, яке я використовую зараз, яке стверджує потік, поки цикл працює, і, отже, менш масштабований, ніж це рішення.
Ерік Штрокен

1

Мені потрібно було викликати повторювані асинхронні завдання з синхронного методу.

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

Це адаптація відповіді Джеффа. Він змінюється, щоб взяти його в. Func<Task> Він також переконує, що період - це те, наскільки часто він виконується, віднімаючи час виконання завдання з періоду для наступної затримки.

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}

0

Я зіткнувся з подібною проблемою і написав TaskTimerклас, який повертає ряд завдань, які виконуються за таймером: https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}

-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

Просто ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.