• C#初级_定时器


    一、引言

    在开发中,会遇到并行处理的需求。
    有时只需要使用task(底层是创建个线程)来处理一下就好了。而有时则在并行处理的基础上还有时间的要求,较常见的就是每隔一定时间处理一次。当然,这用task肯定可以实现,但是时间这块得自己控制,无疑增加了工作量和不确定性。
    .NET提供了叫做定时器(timer,也叫计时器)的类,它在并行处理的基础上,带了时间参数的设置,可以满足这一需求。

    其实本文标题与其叫C#定时器,不如叫.NET定时器好些。因为这边介绍的定时器是.NET中的东西,不只针对C#(不过调用形式是C#的),.NET平台下的其他语言也可以用。不过.NET下的语言,用C#的可能相对多一些,所以很多时候.NET的东西也被叫做C#的东西,当然这种叫法不是很规范。


    二、Timers

    .NET提供了两种定时器用于多线程环境:

    • System.Threading.Timer,它会以固定间隔在ThreadPool线程上执行回调函数。
    • System.Timers.Timer,默认情况下,它会以固定间隔在ThreadPool线程上触发一个事件。

    ⚠注意:
    一些.NET实现下还有其它类型的定时器:

    • System.Windows.Forms.Timer,从名字中就能看出来,它是WinForms的定期触发事件的组件,是为单线程环境设计的。
    • System.Web.UI.Timer,这是一个ASP.NET组件,以固定间隔执行异步或同步的网页回发。
    • System.Windows.Threading.DispatcherTimer,集成到Dispatcher队列中的定时器,它会按照指定的时间间隔和指定的优先级进行处理。

    1. System.Threading.Timer

    1.1. 简单使用

    System.Threading.Timer类能以指定间隔调用委托(连续或单次)。该委托在ThreadPool线程中执行。

    创建System.Threading.Timer对象时,你需要指定一个TimerCallback委托来定义回调方法、一个传递给回调函数的可选state对象、以及首次调用回调函数之前的延迟时间和连续回调调用的时间间隔。要取消一个挂起(pending)的定时器,可以调用Timer.Dispose方法。

    下面代码示例创建了一个定时器,在创建一秒后首次调用委托,之后每两秒调用一次。示例中的state对象用于计算调用委托的次数。当委托被调用10次后,计时器停止。

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    class Program
    {
        private static Timer timer;
    
        static void Main(string[] args)
        {
            var timerState = new TimerState { Counter = 0 };
    
            timer = new Timer(
                callback: new TimerCallback(TimerTask),
                state: timerState,
                dueTime: 1000,
                period: 2000);
    
            while (timerState.Counter <= 10)
            {
                Task.Delay(1000).Wait();
            }
    
            timer.Dispose();
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: done.");
        }
    
        private static void TimerTask(object timerState)
        {
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: starting a new callback.");
            var state = timerState as TimerState;
            Interlocked.Increment(ref state.Counter);
        }
    
        class TimerState
        {
            public int Counter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    1.2. 注意点

    使用TimerCallback委托来指定希望Timer执行的方法。TimerCallback委托的签名如下:

    void TimerCallback(Object state)
    
    • 1

    定时器委托是在定时器被构造之后(即new了之后)指定的,且无法更改。该方法不会在创建定时器的线程上执行;它是在系统提供的ThreadPool线程上执行。

    Timer类有着与系统时钟相同的分辨率。这意味着,如果周期小于系统时钟的分辨率,TimerCallback委托将按照系统时钟的分辨率定义的时间间隔执行,在Win7和Win8系统上大约是15ms。可以使用Change方法更改到期时间和周期,或禁用定时器。

    ⚠注意:
    只要使用Timer,就必须保持对它的引用。
    与其它托管对象一样,当没有对Timer的引用时,Timer也会受到GC(垃圾回收器)的影响。即使Timer仍然处于活动状态,也不会阻止它被回收。
    使用的系统时钟与GetTickCount使用的时钟相同,不受timeBeginPeriod和timeEndPeriod改变的影响。

    当不再需要定时器时,使用Dispose方法释放定时器所持有的资源。注意,回调函数可能在Dispose()方法重载被调用之后才发生,因为定时器队列回调函数由线程池线程执行。可以使用Dispose(waitHandle)方法重载来等待,直到所有回调完成。

    该定时器执行的回调方法是可重入的(多个线程同时执行,第一个线程还没执行完,第二个线程又进去执行了),因为它是在ThreadPool线程上调用的。如果定时器间隔小于执行回调所需的时间,或者如果所有线程池线程都在使用并且多次排队,则可以在两个线程池线程上同时执行回调。

    ⚠注意:
    System.Threading.Timer是一个简单的轻量级定时器,它使用回调方法,由线程池线程提供服务。不建议在WinForms中使用,因为它的回调函数不会在UI线程上发生。System.Windows.Forms.Timer更适用于WinForms。对于基于服务器的定时器功能,可以考虑使用System.Timers.Timer,它会引发事件并具有额外功能。

    2. System.Timers.Timer

    2.1. 概述

    另一个用于多线程环境的定时器是System.Timers.Timer,默认情况下,它会在ThreadPool线程中引发一个事件。

    当创建System.Timers.Timer对象时,可以指定引发事件的时间间隔。使用Enabled属性来指定定时器是否引发事件。如果要指定只引发一次Elapsed事件,将AutoReset设置为false。AutoReset属性的默认值为true,意味着在interval属性定义的时间间隔内会定时引发Elapsed事件。

    🔺2.2. 注意点

    Timer组件是一个基于服务器的定时器,它会在经过Interval属性设置的毫秒数之后引发一个Elapsed事件。使用AutoReset属性配置Timer对象,使其只引发一次或重复引发事件。通常,Timer对象声明在类层级,以便你需要它时,它就在作用域中。

    // 声明在类层级大概是这个意思?
    // 不是定义在局部的,而是整个类的成员变量(字段)
    // 这样你才类中任意地方都可以去操作它
    class A
    {
    	Timer _timer;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后可以处理它的Elapsed事件来进行常规处理。例如,假设你有一个服务器,它必须每周7天、每天24小时运行。你可以创建一个使用Timer对象的服务来定期检查服务器,并确保系统已启动并运行。如果系统没有响应,服务可以尝试重新启动服务器并通知管理员。

    ⚠注意:
    该Timer类并不适用所有的.NET实现和版本,例如,.NET Standard 1.6以及更低版本。在这些情况下,你可以使用System.Threading.Timer类。
    从这句话中有种System.Timers.Timer的使用优先级比System.Threading.Timer高的感觉。

    该Timer类实现了IDisposable接口。当你使用完该类后,应该销毁它。要直接销毁该类,在try/catch块中调用它的dispose方法。间接销毁,可以使用using。

    基于服务器的System.Timers.Timer类是为多线程环境中的工作线程而设计的。服务器定时器可以在线程之间移动来处理引发的Elapsed事件,在引发事件及时性上比Windows定时器更精确。

    System.Timers.Timer组件根据Interval属性的值(以毫秒为单位)引发Elapsed事件。通过处理此事件来执行所需的处理过程。例如,假设你有一个在线销售应用程序,它不断向数据库发布销售订单。编译运输指令的服务对一批订单进行操作而不是单独处理每个订单。你可以使用Timer来每30分钟启动批处理。

    ⚠注意:
    System.Timers.Timer类具有与系统时钟相同的分辨率。
    这意味着如果Interval属性小于系统时钟分辨率,则Elapsed事件将按照系统时钟分辨率定义的时间间隔触发。

    Timer组件捕获并抑制事件处理程序为Elapsed事件抛出的所有异常(也就是说,在Elapsed事件处理器中抛出的异常,你在其它线程中无法直接感受到。这就可能导致,如果你在Elapsed事件处理器中没有添加异常处理机制,并且里面抛出异常了,从线程外看好像啥也没发生)。但是要注意,对于异步执行并包含await操作符(在C#中)或await操作符(在VB中)的事件处理程序则不然。这些事件处理程序中抛出的异常会传回调用线程,如下所示:

    using System;
    using System.Threading.Tasks;
    using System.Timers;
    
    class Example
    {
       static void Main()
       {
          Timer timer = new Timer(1000);
          timer.Elapsed += async ( sender, e ) => await HandleTimer();
          timer.Start();
          Console.Write("Press any key to exit... ");
          Console.ReadKey();
       }
    
       private static Task HandleTimer()
       {
         Console.WriteLine("\nHandler not implemented..." );
         throw new NotImplementedException();
       }
    }
    // The example displays output like the following:
    //   Press any key to exit...
    //   Handler not implemented...
    //   
    //   Unhandled Exception: System.NotImplementedException: The method or operation is not implemented.
    //      at Example.HandleTimer()
    //      at Example.<
    b__0>d__2.MoveNext() // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<>c__DisplayClass2.b__5(Object state) // at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) // at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) // at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() // at System.Threading.ThreadPoolWorkQueue.Dispatch()
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    若SynchronizingObject属性为null,则Elapsed事件在ThreadPool线程中引发。若Elapsed事件的处理耗时长于Interval,则该事件可能会在另一个ThreadPool线程上再次引发。该情况下,事件处理程序应该是可重入的(reentrant)。

    ⚠注意:
    事件处理方法可能运行在一个线程,同时另一个线程调用Stop方法或将Enabled属性设置为false。这可能导致在定时器停止后引发Elapsed事件。Stop方法的示例代码展示了一种避免这种竞争条件的方法。

    后面这部分等学了await再看。

    三、总结

    总的来说,System.Threading.Timer和System.Timers.Timer表面上主要异同是,
    1️⃣前者是直接调用委托,而后者是引发事件。
    2️⃣两者都是运行在系统线程池线程上的
    3️⃣两者时钟分辨率都等于系统时钟分辨率
    4️⃣前者可能较轻量级,后者是服务器级别的(但这点很模糊,我的理解是一般的桌面程序用前者即可,如果程序相对较大,可能用后者好)
    5️⃣System.Timers.Timer会捕获并抑制Elapsed事件处理器抛出的所有异常。

    如果你想将这两者用于WPF应用,尤其是MVVM的VM中来实现多线程改变绑定的数据,那可能会达不到预期效果,因为在WPF框架的设定下,非UI线程直接或间接访问UI线程是不合法的。如ObservableCollection之类的集合跨线程访问时,大多会报错System.NotSupportedException。

    最后根据官方文档描述,一般都是推荐用后者的。

  • 相关阅读:
    【Ajax进阶】跨域和JSONP的学习
    5. 用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备
    [CSS]CSS 的背景
    ES6学习笔记
    【Unity3D赛车游戏制作】设置面板搭建——UGUI复合控件Toggle
    【数据结构】时间复杂度和空间复杂度
    promise结合requestAnimationFrame用法示例
    java EE初阶 —— 线程的安全问题
    Windows ssh免密访问Linux服务器
    Open3D(C++) 读取、可视化并保存mesh
  • 原文地址:https://blog.csdn.net/BadAyase/article/details/126384462