Есть ли замена на основе задач для System.Threading.Timer? - PullRequest
82 голосов
/ 03 февраля 2011

Я новичок в Задачах .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 ]

74 голосов
/ 22 мая 2014

Это зависит от 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 в качестве источника завершения задачи используется таймер истечения.

56 голосов
/ 19 февраля 2013

UPDATE Я помечаю ответ ниже как "ответ", так как он достаточно стар, чтобы мы могли использовать шаблон async / await. Не нужно больше понижать это. LOL


Как ответила Эми, реализация периодических / таймерных задач не основана на задачах. Однако, основываясь на моем первоначальном ОБНОВЛЕНИИ, мы превратили это в нечто весьма полезное и проверенное на производстве. Мысль я бы поделился:

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 . . .
12 голосов
/ 04 июня 2012

Это не совсем в System.Threading.Tasks, но Observable.Timer (или проще Observable.Interval) из библиотеки Reactive Extensions, вероятно, то, что вы ищете.

8 голосов
/ 18 августа 2016

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

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

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

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

  1. Добавьте ConfigureAwait(false) к Task.Delay(), чтобы выполнить действие doWork в потоке пула потоков, в противном случае doWork будет выполняться в вызывающем потоке, что не является идеей параллелизма
  2. Придерживайтесь шаблона отмены, создав исключение TaskCanceledException (все еще требуется?)
  3. Переслать CancellationToken на 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);
    }
0 голосов
/ 01 мая 2019

Мне нужно было запускать повторяющиеся асинхронные задачи из синхронного метода.

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 голосов
/ 20 июня 2018
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; }
}

Простой ...

0 голосов
/ 05 сентября 2017

Я столкнулся с подобной проблемой и написал 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();
    }
}
...