是否有基于任务的System.Threading.Timer替代品?


88

我是.Net 4.0的Task的新手,却找不到我认为是基于Task的计时器的替代品或实现,例如周期性Task。有这样的事吗?

更新 我提出了我认为是我需要的解决方案,该解决方案是将“计时器”功能包装在带有子任务的Task内,并全部利用CancellationToken并返回Task以便能够参与进一步的Task步骤。

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
您应该在Task中使用Timer而不是使用Thread.Sleep机制。效率更高。
约安。B

Answers:


84

它取决于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.Inde的内部,Delay使用计时器到期作为任务完成源。


1
我刚刚切换到这种方法。但我有条件地action()重复打!cancelToken.IsCancellationRequested。更好吧?
HappyNomad

3
谢谢您的配合-我们使用的是相同的选项,但已将延迟时间推迟到采取行动之后(这对我们来说更有意义,因为我们需要立即致电该行为,然后在x之后重复)
Michael Parker

1
谢谢你 但是这段代码不会“每X小时”运行,而是“每X小时+ action执行时间”运行,对吗?
亚历克斯(Alex)

正确。如果要考虑执行时间,则需要一些数学运算。但是,如果执行时间超出您的时间范围,则可能会变得棘手,等等...
Jeff

57

更新将下面的答案标记为“答案”,因为它已经足够老了,现在我们应该使用异步/等待模式。无需再对此进行投票。大声笑


正如Amy回答的那样,没有基于任务的周期/计时器实现。但是,基于我最初的UPDATE,我们已经将其演变为相当有用的东西,并进行了生产测试。我想分享一下:

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类可以利用异步/等待来面向.Net 4.5的应用程序,但是对于我们来说,我们还不能迁移到.Net 4.5。此外,PeriodicTaskFactory还提供了一些其他的“计时器”终止机制,例如最大迭代次数和最大持续时间,并提供了一种确保每次迭代可以等待最后一次迭代的方式。但是,当我们移至.Net 4.5
Jim

4
+1,谢谢!我现在正在使用您的课程。但是,要使其与UI线程配合使用,我必须TaskScheduler.FromCurrentSynchronizationContext()在设置之前进行调用mainAction。然后,将生成的调度程序传递MainPeriodicTaskAction给它以创建subTaskwith。
HappyNomad

2
我不确定,当线程可以做有用的工作时,这是个好主意。“ Thread.Sleep(delayInMilliseconds)”,“ periodResetEvent.Wait(intervalInMilliseconds,cancelToken)” ...然后,您使用计时器,在硬件中等待,因此不会浪费任何线程。但是在您的解决方案中,线程是一无是处。
RollingStone

2
@rollingstone我同意。我认为这种解决方案在很大程度上破坏了类似异步行为的目的。使用计时器而不浪费线程会更好。这只是给异步的外观而没有任何好处。
杰夫


9

到目前为止,我将LongRunning TPL任务用于CPU绑定的循环后台工作,而不是线程计时器,因为:

  • TPL任务支持取消
  • 在关闭程序时,线程计时器可能会启动另一个线程,从而可能导致资源处置出现问题
  • 发生溢出的机会:由于意外的长时间工作,线程计时器可能会启动另一个线程,而前一个线程仍在处理中(我知道,可以通过停止并重新启动计时器来防止此线程)

但是,TPL解决方案始终要求使用专用线程,而在等待下一个操作时(大多数情况下),该线程不是必需的。我想使用提出的Jeff解决方案在后台执行CPU绑定的循环工作,因为只有在有工作要做时才需要线程池线程,这对于可伸缩性更好(尤其是在间隔时间很大时)。

为此,我建议进行4种调整:

  1. 添加ConfigureAwait(false)到对线程池线程Task.Delay()执行doWork操作,否则doWork将在调用线程上执行,这不是并行性的想法
  2. 通过抛出TaskCanceledException坚持取消模式(还需要吗?)
  3. 转发CancellationToken以doWork使其能够取消任务
  4. 添加对象类型的参数以提供任务状态信息(例如TPL任务)

关于第二点,我不确定,异步等待是否仍然需要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()调用包装在a中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);
    }

using ConfigureAwait(false)将把方法的继续安排到线程池中,因此它并不能真正解决线程计时器的第二点。我也认为taskState没有必要;lambda变量捕获更加灵活且类型安全。
史蒂芬·克利西

1
我真正想要做的是交换await Task.Delay()doWork()因此doWork()可以在启动期间立即执行。但是如果没有任何技巧,doWork()它将在调用线程上第一次执行并阻止它。斯蒂芬,你有解决这个问题的办法吗?
Erik Stroeken

1
最简单的方法是将整个内容包装在一个文件中Task.Run
史蒂芬·

是的,但是我可以回到我现在使用的TPL解决方案,该解决方案在循环运行时声明一个线程,因此可扩展性比该解决方案差。
Erik Stroeken '16

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}");
    }
}


-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; }
}

简单...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.