C#初级_定时器

17 篇文章 11 订阅

一、引言

在开发中,会遇到并行处理的需求。
有时只需要使用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. 注意点

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

void TimerCallback(Object state)

定时器委托是在定时器被构造之后(即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;
}

然后可以处理它的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.<<Main>b__0>d__2.MoveNext()
//   --- End of stack trace from previous location where exception was thrown ---
//      at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<>c__DisplayClass2.<ThrowAsync>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()

若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。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值