首页 > 代码库 > .NET的内建定时器类型是否会发生回调方法冲入
.NET的内建定时器类型是否会发生回调方法冲入
分析问题
所谓的方法重入,是一个有关多线程编程的概念。程序中多个线程同时运行时,就可能发生同一个方法被多个线程同时调用的情况。当这个方法中存在一些非线程安全的代码时,方法重入就会导致数据不一致的情况,这是非常严重的Bug。
在前文中,笔者已经简要介绍了.NET的内建定时器类型,它们是:
1、System.Windows.Forms.Timer。
2、System.Threading.Timer。
3、System.Timers.Timer。
这三种类型的计时方法是不同的,这里笔者分别分析了三种类型是否会存在方法重入的情况。
1、System.Windows.Forms.Timer类型。
在前文中笔者已经介绍了,System.Windows.Forms.Timer类型的计时机制实在当前UI线程的消息队列里插入一条定时消息,这样的机制保证了不破坏单线程的运行环境。在这种情况下,计时定时器的时间间隔被设置的最小,后一个定时消息必须等待前一个消息处理完毕。所以在这种情况下是不会发生回调方法重入的情况的。
2、System.Threading.Timer的回调方法在一个工作者线程上执行,每当一个定时事件发生时,控制System.Threading.Timer对象的线程就会负责从线程池中分配一个新的工作者线程,这是一种典型的多线程编程环境,所以方法重入的现象是可能发生的。这就需要程序员在编写System.Threading.Timer类型对象的回调方法时,注意线程同步的问题。
3、System.Timers.Timer类型。
System.Timers.Timer类型可以看作System.Threading.Timer的一个封装类型,其可以通过同步块设置属性,这个时候,其特性和System.Windows.Forms.Timer非常类似,并且不会发生回调方法重入的情况。当当其同步快属性未设定时,它的回调方法就会在一个工作者线程上被执行,这时候,它的回调方法就可能产生重入的情况。
以下代码展示了System.Threading.Timer类型和System.Timers.Timer类型的重入情况。
using System;namespace Test{ class Reenter { //用来造成线程同步问题的静态成员 private static int TestInt1 = 0; private static int TestInt2 = 0; static void Main() { Console.WriteLine("System.Timers.Timer回调方法重入测试:"); TimersTimerReenter(); //这里确保已经开始的回调方法有机会结束 System.Threading.Thread.Sleep(2000); Console.WriteLine("System.Threading.Timer回调方法重入测试:"); ThreadingTimerReenter(); Console.Read(); } /// <summary> /// 展示System.Timers.Timer的回调方法重入 /// </summary> static void TimersTimerReenter() { System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 100;//100毫秒 timer.Elapsed += TimersTimerHandler; timer.Start(); System.Threading.Thread.Sleep(2000);//运行2秒 timer.Stop(); } /// <summary> /// 展示System.Threading.Timer的回调方法重入 /// </summary> static void ThreadingTimerReenter() { using (System.Threading.Timer timer=new System.Threading.Timer(new System.Threading.TimerCallback (ThreadingTimerHandler),null,0,100)) { System.Threading.Thread.Sleep(2000);//运行2秒 } } static void ThreadingTimerHandler(object state) { Console.WriteLine("测试整数:{0}",TestInt2.ToString()); //睡眠10s,保证方法重入 System.Threading.Thread.Sleep(10000); TestInt2++; Console.WriteLine("自增1后测试整数:{0}",TestInt2.ToString()); } /// <summary> /// System.Timers.Timer的回调方法 /// </summary> static void TimersTimerHandler(object sender, EventArgs e) { Console.WriteLine("测试整数:{0}",TestInt1.ToString()); //睡眠10s,保证方法重入 System.Threading.Thread.Sleep(10000); TestInt1++; Console.WriteLine("自增1后测试整数:{0}",TestInt1.ToString()); } }}
在以上代码中,为了保证定时器回调方法的执行时间长于定时器的间隔时间,添加了让线程睡眠1s的代码:
System.Threading.Thread.Sleep(1000);
在这种情况下,输出将和预期的有很大不同,多个回调方法将并行地执行并且无法控制其顺序:
正如输出所显示的,所有回调方法并行执行的结果是执行顺序杂乱无章,并且操作的全局变量可能会在其他线程中被修改。为了避免发生这种情况,程序员需要为回调方法添加lock锁,下面的代码展示了这一做法:
private static object lockObj = new object(); /// <summary> /// System.Threading.Timer的回调方法 /// </summary> /// <param name="state"></param> static void ThreadingTimerHandler(object state) { lock (lockObj) { Console.WriteLine("测试整数:{0}", TestInt2.ToString()); //睡眠10s,保证方法重入 System.Threading.Thread.Sleep(10000); TestInt2++; Console.WriteLine("自增1后测试整数:{0}", TestInt2.ToString()); } } /// <summary> /// System.Timers.Timer的回调方法 /// </summary> static void TimersTimerHandler(object sender, EventArgs e) { lock (lockObj) { Console.WriteLine("测试整数:{0}", TestInt1.ToString()); //睡眠10s,保证方法重入 System.Threading.Thread.Sleep(10000); TestInt1++; Console.WriteLine("自增1后测试整数:{0}", TestInt1.ToString()); } }
在加了同步锁的情况下,可以保证所有时间只有一个线程可以执行回调方法,而其他线程将会被迫阻塞等待,这是加锁后的输出:
如读者看到的,加锁后的输出是有规律的,线程同步的问题得到了解决,但是运行程序的时候读者也可能已经感觉到了,加锁本质上破获了多线程并行优势,使得程序的执行变得相对缓慢。所以程序员在编写定时器代码时,应仔细考虑何时需要加锁,而合适需要确保多线程并行运行。
答案
在.NET的内建定时器中,System.Timers.Timer和System.Threading.Timer两个类型可能发生回调方法重入的问题,而System.Windows.Forms.Timer则不存在这个问题。
在定时器设计时,需要考虑是否需要为回调方法加锁和如何加锁,原则上被加锁的代码越少,则对效率的影响也越小。
.NET的内建定时器类型是否会发生回调方法冲入