.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则不存在这个问题。

  在定时器设计时,需要考虑是否需要为回调方法加锁和如何加锁,原则上被加锁的代码越少,则对效率的影响也越小。    

 

 

  

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。