首页 > 代码库 > .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的内建定时器类型是否会发生回调方法冲入