添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
力能扛鼎的盒饭  ·  java ...·  1 年前    · 
爱运动的太阳  ·  警告 C6001 | Microsoft ...·  2 年前    · 

C#中timer类的用法(含timer为什么只执行了一次)

专栏 / C#中timer类的用法(含timer为什么只执行了一次)

C#中timer类的用法(含timer为什么只执行了一次)

2022年06月11日 16:09 --阅读 · --喜欢 · 不想躺平得躺平青年
粉丝: 133 文章: 6

C#中timer类的用法

1、System.Windows.Forms.Timer

实现按用户定义的时间间隔引发事件的计时器。此计时器最宜用于 Windows 窗体应用程序中,并且必须在窗口中使用。

2、System.Threading.Timer

提供以指定的时间间隔执行方法的机制。无法继承此类。

3、System.Timers.Timer

在应用程序中生成定期事件。

这三个定时器位于不同的命名空间内,上面大概介绍了3个定时器的用途,其中第一个是只能在Windows窗体中使用的控件。

在.NET1.1里面,第3个System.Timers.Timer,也是可以拖拽使用,而.NET2.0开始取消了,只能手动编写代码。而后2个没有限制制。

下面通过具体的列子来看3个Timer的使用和区别

一 、System.Windows.Forms.Timer

复制代码

#region System.Windows.Forms.Timer      public partial class Form1 : Form        public Form1()         InitializeComponent();        int num = 0;        private void Form_Timer_Tick(object sender, EventArgs e)            label1.Text = (++num).ToString();            Thread.Sleep(3000);        private void button1_Click(object sender, EventArgs e)            Form_Timer.Start();        private void button2_Click(object sender, EventArgs e)            Form_Timer.Stop();    #endregion

复制代码

上面这个是一个很简单的功能,在Form窗体上拖了一个System.Windows.Forms.Timer控件名字为Form_Timer,在属性窗中把Enable属性设置为Ture,Interval是定时器的间隔时间。双击这个控件就可以看到 Form_Timer_Tick方法。

在这个方法中,我们让她不停的加一个数字并显示在窗体上,2个按钮提供了对计时器的控制功能。执行的时候你去点击其他窗体在回来,你会发现我们的窗体失去响应了。

因为我们这里使用Thread.Sleep(3000); 让当前线程挂起,而UI失去相应,说明了这里执行时候采用的是单线程 。也就是执行定时器的线程就是UI线程。Timer 用于以用户定义的事件间隔触发事件。

Windows 计时器是为单线程环境设计的,其中,UI 线程用于执行处理。它要求用户代码有一个可用的 UI 消息泵,而且总是在同一个线程中操作,或者将调用封送到另一个线程。

在Timer内部定义的了一个Tick事件,我们前面双击这个控件时实际是增加了一行代码。

this.Form_Timer.Tick += new System.EventHandler(this.Form_Timer_Tick);

然后Windows将这个定时器与调用线程关联(UI线程)。当定时器触发时,Windows把一个定时器消息插入到线程消息队列中。调用线程执行一个消息泵提取消息,然后发送到回调方法中(这里的Form_Timer_Tick方法)。

而这些都是单线程进行了,所以在执行回调方法时UI会假死。所以使用这个控件不宜执行计算受限或IO受限的代码,因为这样容易导致界面假死,而应该使用多线程调用的Timer。

另外要注意的是这个控件时间精度不高,精度限定为 55 毫秒。

二、System.Timers.Timer

接下来就看下另一个Timer,我们用它来改写上面的程序

复制代码

#region System.Windows.Forms.Timer      public partial class Form1 : Form        public Form1() {            InitializeComponent();        int num = 0;        DateTime time1 = new DateTime();        DateTime time2 = new DateTime();        //定义Timer          System.Timers.Timer Timers_Timer = new System.Timers.Timer();        private void button1_Click(object sender, EventArgs e)            //手动设置Timer,开始执行              Timers_Timer.Interval = 20;            Timers_Timer.Enabled = true;            Timers_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timers_Timer_Elapsed);            time1 = DateTime.Now;        void Timers_Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)            label1.Text = Convert.ToString((++num)); //显示到lable              Thread.Sleep(3000);        private void button2_Click(object sender, EventArgs e)            //停止执行              Timers_Timer.Enabled = false;            time2 = DateTime.Now;            MessageBox.Show(Convert.ToString(time2 - time1));    #endregion

复制代码

我们可以看到这个代码和前面使用Form.Timer的基本相同,不同的是我们是手动定义的对象,而不是通过拉控件。他也有Interval ,Enabled 等属性,作用和第一是一样的。

不同的是他的事件名为Elapsed ,但是和上面的Tick一样,绑定一个委托的方法。只是这里我们是手动完成的。另外不同之处是Form.Timer我们可以用Stop和Start方法控制,而这里是通过Enable属性控制。

但实际上也可以用Stop和Start方法,内部也是通过他自己的Enable来控制的。

最大的不同就是上面的代码在调试时会报错,提示你"线程间操作无效: 从不是创建控件“label1”的线程访问它。"但如果你不调试直接运行是OK的,而且运行时你去拖动窗体会发现没有出现假死。

从这里我们就可以知道这里的Timer的创建线程和执行线程不是同一个线程。也就是使用了多线程。 Timer的创建线程是UI线程,而执行线程是TheardPool中的线程 所以不会假死,但调试的时候会报错,因为非控件的创建线程不能操作控件

但你可以直接运行,这里是VS05做了手脚。解决办法很多,用delegate.BeginInvoke()等等。这里介绍特有的一种方法,设置Timer的SynchronizingObject属性,Timers_Timer.SynchronizingObject = label1;

这样调试运行时就不会报错了,但是设置了这个属性Timer就编程单线程调用了,就基本和第一个完全一样了。

/// 在timer中使用SynchronizingObject的原因(tangtao_xp的注解)
/// 请参考https://msdn.microsoft.com/en-us//library/system.timers.timer.synchronizingobject
/// 1.如果SynchronizingObject为null,timer的Elapsed事件会默认由线程池进行处理
/// 2.如果timer的Elapsed事件要在windows Form等UI元件中处理,就会出现UI线程访问线程池情况
/// 此时会引发错误和异常;将SynchronizingObject设置成要处理timer的Elapsed事件的UI元件,
/// 该timer就会由该UI元件的线程创建,从而避免异常。

三 、System.Threading.Timer

继续用这个对象改造程序。

复制代码

#region System.Windows.Forms.Timer      public partial class Form1 : Form        public Form1()            InitializeComponent();        int num = 0;        DateTime time1 = new DateTime();        DateTime time2 = new DateTime();        System.Threading.Timer Thread_Time;        private void button1_Click(object sender, EventArgs e)            Thread_Time = new System.Threading.Timer(Thread_Timer_Method, null, 0, 20);            time1 = DateTime.Now;        void Thread_Timer_Method(object o)            label1.Text = Convert.ToString((++num));            System.Threading.Thread.Sleep(3000);        private void button2_Click(object sender, EventArgs e)            Thread_Time.Dispose();            time2 = DateTime.Now;            MessageBox.Show(Convert.ToString(time2 - time1));    #endregion

复制代码

用Threading.Timer时的方法,和前面就不太相同了,所以的参数全部在构造函数中进行了设置,而且可以设置启动时间。而且没有提供start和stop方法来控制计时器。

而且是以一种回调方法的方式实现,而不是通过事件来实现的。他们之间还是有区别的。我们只有销毁掉对象来停止他。

当你运行时,你会发现他和前面的Timers.Timer一样,是多线程的,主要表现在不会假死,调试运行报错。但跟让你奇怪的是,我们的代码竟然无法让她停止下来。

调用了Dispose方法没有用。问题在那?然后有进行了测试,修改了间隔时间为100,200,500,1000,3000,4000。

这几种情况。发现当间隔为500ms以上是基本马上就停止了。而间隔时间相对执行时间越短,继续执行的时间越长。这应该是在间隔时间小于执行时间时多个线程运行造成的。因为所有的线程不是同时停止的。间隔越短,线程越多,所以执行次数越多。

最后来看下这个对象另外一个特殊的地方。

复制代码

static void Main()        Timer t = new Timer(Test, null, 0, 1000);        Console.ReadLine();    public static void Test(object o)        Console.WriteLine("hello world");        GC.Collect();    }

复制代码

这段代码会输出什么结果呢? 默认情况他只输出一次,就停止了 。为什么呢?根据上面说的,当定义对象t,执行代码后,进行了强制垃圾回收,因为t在Main中没有其他引用,所以被回收掉了。

但是如果我们把编译器的”优化“项取消掉,在看看情况。程序进然一直在输出。为什么执行垃圾回收却没有被回收呢?因为这个禁用优化选项,t的声明周期被扩展到了方法结束。所以一直执行。

因为编译器默认是优化的,所以我们必须保证Timer对象一直被引用,而避免被垃圾回收。所以我们可以在编译器打开优化的情况下,在Main函数最后加上t=null保证回收前被引用,但你发现,这样是没用的。

因为JIT编译器优化后会吧t=null直接删除,所以我们用t.Dispose(),就可以达到目的。在我们进行垃圾回收时,CLR发现t还有被引用,还没执行Dispose所以不会被回收。

是以Threading.Timer有时候会出现运行一次就停止或者是销毁了还在运行的情况,而且和编译器优化也有关,所以使用时要注意。

最后看下MSDN的描述: 只要在使用 Timer,就必须保留对它的引用。对于任何托管对象,如果没有对 Timer 的引用,计时器会被垃圾回收。即使 Timer 仍处在活动状态,也会被回收。当不再需要计时器时,请使用 Dispose 方法释放计时器持有的资源。

如果希望在计时器被释放时接收到信号,请使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重载。计时器已被释放后,WaitHandle 便终止。

总结:

  • System.Threading.Timer 是一个简单的轻量计时器,它使用回调方法并由线程池线程提供服务。不建议将其用于 Windows 窗体,因为其回调不在用户界面线程上进行。System.Windows.Forms.Timer 是用于 Windows 窗体的更佳选择。

  • 要获取基于服务器的计时器功能,可以考虑使用 System.Timers.Timer,它可以引发事件并具有其他功能。

  • 在《CLR Via C#》中讲多线程时有提到这3个计时器,但作者说System.Timers.Timer是对System.Threading.Timer的报装,不推荐使用,但是在我的WEB项目中的Application_Start中我还是使用的这个而不是Threading.Timer,因为使用Threading.Timer时只执行了一次就不在执行了。

  • 对于计时器在B/S结构中的使用就复杂一些,一般我们把计时器放在Application_OnStart中,这样全局维护一个计时器,可以进行定期备份数据库,定期维护用户等操作,而且方法写作静态的,以免被垃圾回收

  • 而不建议在一般的aspx页面中使用,因为服务器端的定时器对用户这样意义不大,完全可以使用JS代替。而且这个页面的每个请求都可能引入一个新的定时器,导致系统崩溃。

  • 另外, 定时器是ASP.NET进程,IIS有关,所以对用重要的执行任务,还是建议写成服务或独立程序放在服务器上执行好了。

  • System.Windows.Forms.Timer 是应用于WinForm中的,它是通过Windows消息机制实现的,类似于VB或Delphi中的Timer控件,内部使用API  SetTimer实现的。它的主要缺点是计时不精确,而且必须有消息循环,Console  Application(控制台应用程序)无法使用。

  • System.Timers.Timer和System.Threading.Timer 非常类似,它们是通过.NET  Thread  Pool实现的,轻量,计时精确,对应用程序、消息没有特别的要求。System.Timers.Timer还可以应用于WinForm,完全取代上面的Timer控件。它们的缺点是不支持直接的拖放,需要手工编码。


投诉或建议
创建私有Alas源码仓库
前言 Alas 的源代码托管在 GitHub 上 ,为了照顾 CN 大陆网友 ,开发组将其分发在国内代码托管平台,如 gitee ,coding 。但由于现在 Alas 的用户接近 3W 人,国内代码托管平台没有 GitHub 那样财大气粗,当负载流量过多时,就会封禁官方 Alas 的仓库 为了解决 Alas 镜像不断流浪的问题,这篇教程主要是为了教会大家如何创建自己的私有仓库,更新 Alastips: 肉身在繁中地区,国外的,可以直连 GitHub ,请使用稳定的 GitHub 仓库 不推荐使用 ste
018、大厂面试题:JVM中都有哪些常见的垃圾回收器,各自的特点是什么?
大厂面试题JVM中都有哪些常见的垃圾回收器,各自的特点是什么?1、前文回顾上一篇文章我们给大家分析了一下到底什么时候会触发Minor GC,什么时候会让对象从新生代转移到老年代,包括为了新生代转移到老年代的内存足够安全,Minor GC之前要如何检查老年代的内存空间,在什么情况下会触发老年代的Full GC,老年代的垃圾回收算法是什么,这些问题都已经给大家分析清楚了。这篇文章,我们先接着上篇文章,给大家来一个真实的我们之前一个生产系统的老年代频繁Full GC的案例,让大家更加透彻的理解整个对象分配以及转