Есть ли замена 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

Ответы:

87

Это зависит от 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 04
3
Спасибо за это - мы используем то же самое, но переместили задержку до окончания действия (для нас это имеет больше смысла, поскольку нам нужно вызвать действие немедленно, а затем повторить после x)
Майкл Паркер
2
Спасибо за это. Но этот код не будет запускаться «каждые X часов», он будет запускаться «каждые X часов + время actionвыполнения», я прав?
Alex
Верный. Если вы хотите учесть время выполнения, вам понадобится немного математики. Однако это может стать сложным, если время выполнения превышает ваш период и т. Д.
Джефф
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 01
1
@HappyNomad, похоже, что класс PeriodicTaskFactory может использовать async / await для приложений, ориентированных на .Net 4.5, но для нас мы пока не можем перейти на .Net 4.5. Кроме того, PeriodicTaskFactory предоставляет некоторые дополнительные механизмы завершения «таймера», такие как максимальное количество итераций и максимальную продолжительность, а также обеспечивает способ гарантировать, что каждая итерация может ждать до последней итерации. Но я постараюсь адаптировать это для использования async / await, когда мы перейдем на .Net 4.5
Джим
4
+1 Сейчас пользуюсь вашим классом, спасибо. Однако, чтобы заставить его работать с потоком пользовательского интерфейса, мне нужно позвонить TaskScheduler.FromCurrentSynchronizationContext()перед настройкой mainAction. Затем я передаю получившийся планировщик, MainPeriodicTaskActionчтобы он создал файл subTaskwith.
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)). Subscribe (v => Debug.WriteLine (v));
Мартин Каподичи
1
Хорошо, но можно ли отключить эти реактивные конструкции?
Кот Шмиль
9

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

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

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

Для этого я бы предложил 4 варианта:

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

Насчет пункта 2 я не уверен, async await по-прежнему требует 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() и ожидая его.

Ниже представлена ​​улучшенная замена async \ 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}");
    }
}
chris31389
источник
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; }
}

Просто...

ним
источник