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

Posted

技术标签:

【中文标题】System.Threading.Timer 是不是有基于任务的替代品?【英文标题】:Is there a Task based replacement for System.Threading.Timer?System.Threading.Timer 是否有基于任务的替代品? 【发布时间】:2011-06-20 22:14:34 【问题描述】:

我是 .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);
      

【问题讨论】:

你应该在Task里面使用一个Timer而不是使用Thread.Sleep机制。效率更高。 【参考方案1】:

这取决于 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 使用计时器到期作为任务完成源。

【讨论】:

我刚刚切换到这种方法。但我有条件地打电话给action(),并重复!cancelToken.IsCancellationRequested。这样更好,对吧? 谢谢 - 我们使用相同的,但已将延迟移到操作之后(这对我们来说更有意义,因为我们需要立即调用操作,然后在 x 之后重复) 谢谢。但是这段代码不会“每 X 小时”运行一次,它会“每 X 小时+action 执行时间”运行,对吗? 正确。如果要考虑执行时间,则需要一些数学知识。但是,如果执行时间超过您的期限等,这可能会变得很棘手...... 如果我想要多个周期性任务并行运行,我是否只使用Parallel.Invoke() 来执行它们?【参考方案2】:

更新 我是marking the answer below 作为“答案”,因为它已经足够老了,现在我们应该使用异步/等待模式。没有必要再否决这个了。哈哈


正如 Amy 所回答的,没有基于任务的定期/计时器实现。然而,根据我最初的更新,我们已经将它演变成非常有用的东西并且经过了生产测试。想我会分享:

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 . . .

【讨论】:

这看起来像很棒的代码,但我想知道现在有 async/await 关键字是否有必要。您的方法与此处的方法相比如何:***.com/a/14297203/122781? @HappyNomad,看起来 PeriodicTaskFactory 类可以利用异步/等待针对 .Net 4.5 的应用程序,但对我们来说,我们还不能迁移到 .Net 4.5。此外,PeriodicTaskFactory 提供了一些额外的“计时器”终止机制,例如最大迭代次数和最大持续时间,并提供了一种确保每次迭代都可以等待最后一次迭代的方法。但是当我们迁移到 .Net 4.5 时,我会考虑调整它以使用 async/await +1 我现在正在使用您的课程,谢谢。不过,为了让它与 UI 线程配合得很好,我必须在设置 mainAction 之前调用 TaskScheduler.FromCurrentSynchronizationContext()。然后我将生成的调度程序传递给MainPeriodicTaskAction,以便它创建subTask 我不确定,当线程可以做有用的工作时,阻塞线程是个好主意。 "Thread.Sleep(delayInMilliseconds)", "periodResetEvent.Wait(intervalInMilliseconds, cancelToken)"... 然后你使用定时器,你在硬件中等待,所以没有线程花费。但是在您的解决方案中,线程是白费的。 @rollingstone 我同意。我认为这个解决方案在很大程度上违背了类似异步行为的目的。更好地使用计时器而不浪费线程。这只是给出了异步的外观,没有任何好处。【参考方案3】:

它并不完全在 System.Threading.Tasks 中,但来自 Reactive Extensions 库的 Observable.Timer(或更简单的 Observable.Interval)可能是您正在寻找的。​​p>

【讨论】:

例如Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(v => Debug.WriteLine(v)); 很好,但是那些反应式结构可以调用吗?【参考方案4】:

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

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

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

为此,我建议进行 4 种改编:

    ConfigureAwait(false)添加到Task.Delay()以在线程池线程上执行doWork操作,否则doWork将在调用线程上执行,这不是并行的想法 通过抛出 TaskCanceledException 坚持取消模式(仍然需要?) 将 CancellationToken 转发到 doWork 以使其能够取消任务 添加对象类型的参数以提供任务状态信息(如 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);
    

请向您的 cmets 提出建议的解决方案...

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 是必要的; lambda 变量捕获更灵活且类型安全。 我真正想做的是交换await Task.Delay()doWork() 这样doWork() 将在启动期间立即执行。但是如果没有一些技巧,doWork() 将第一次在调用线程上执行并阻止它。斯蒂芬,你有解决这个问题的办法吗? 最简单的方法是把整个东西包在Task.Run中。 是的,但是我可以回到我现在使用的 TPL 解决方案,只要循环正在运行,它就会声明一个线程,因此与此解决方案相比,它的可扩展性较差。【参考方案5】:

我需要从同步方法触发重复的异步任务。

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

这是对 Jeff 答案的改编。改为接收Func&lt;Task&gt; 它还通过从下一个延迟的周期中减去任务的运行时间来确保周期是它运行的频率。

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

【讨论】:

【参考方案6】:

我遇到了类似的问题并编写了一个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();
    

【讨论】:

【参考方案7】:
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; 

简单...

【讨论】:

以上是关于System.Threading.Timer 是不是有基于任务的替代品?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 System.Timers.Timer 能在 GC 中存活,而 System.Threading.Timer 不能?

System.Threading.Timer 杀死 PowerShell 控制台

System.Windows.Forms.TimerSystem.Timers.TimerSystem.Threading.Timer

如何在 C# 中创建计时器而不使用 System.Timers.Timer 或 System.Threading.Timer

System.Threading.Timer:它为啥讨厌我?

System.Threading.Timer 在 25-30 次后停止 [重复]