• 使用timer定时器,防止事件重入


    首先简单介绍一下timer,这里所说的timer是指的System.Timers.timer,顾名思义,就是可以在指定的间隔是引发事件。官方介绍在这里,摘抄如下:

    1
    2
    Timer 组件是基于服务器的计时器,它使您能够指定在应用程序中引发 Elapsed 事件的周期性间隔。然后可通过处理这个事件来提供常规处理。 例如,假设您有一台关键性服务器,必须每周 7 天、每天 24 小时都保持运行。 可以创建一个使用 Timer 的服务,以定期检查服务器并确保系统开启并在运行。 如果系统不响应,则该服务可以尝试重新启动服务器或通知管理员。
        基于服务器的 Timer 是为在多线程环境中用于辅助线程而设计的。 服务器计时器可以在线程间移动来处理引发的 Elapsed 事件,这样就可以比 Windows 计时器更精确地按时引发事件。

    如果想了解跟其他的timer有啥区别,可以看这里,里面有详细的介绍,不再多说了(其实我也不知道还有这么多)。那使用这个计时器有啥好处呢?主要因为它是通过.NET Thread Pool实现的、轻量、计时精确、对应用程序及消息没有特别的要求。

    下面就简单介绍一下,这个Timer是怎么使用的,其实很简单,我就采用微软提供的示例来进行测试,直接上代码了:

    复制代码
    //Timer不要声明成局部变量,否则会被GC回收
     private static System.Timers.Timer aTimer;
     
     public static void Main()
     {
         //实例化Timer类,设置间隔时间为10000毫秒; 
         aTimer = new System.Timers.Timer(10000);
     
         //注册计时器的事件
         aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
     
         //设置时间间隔为2秒(2000毫秒),覆盖构造函数设置的间隔
         aTimer.Interval = 2000;
     
         //设置是执行一次(false)还是一直执行(true),默认为true
         aTimer.AutoReset = true;
     
         //开始计时
         aTimer.Enabled = true;
     
         Console.WriteLine("按任意键退出程序。");
         Console.ReadLine();
     }
     
     //指定Timer触发的事件
     private static void OnTimedEvent(object source, ElapsedEventArgs e)
     {
         Console.WriteLine("触发的事件发生在: {0}", e.SignalTime);
     }
    复制代码

    结果

    复制代码
    /*
    按任意键退出程序。
    触发的事件发生在: 2014/12/26 星期五 23:08:51
    触发的事件发生在: 2014/12/26 星期五 23:08:53
    触发的事件发生在: 2014/12/26 星期五 23:08:55
    触发的事件发生在: 2014/12/26 星期五 23:08:57
    触发的事件发生在: 2014/12/26 星期五 23:08:59
    */
    复制代码

    重入问题重现及分析

    什么叫重入呢?这是一个有关多线程编程的概念:程序中,多个线程同时运行时,就可能发生同一个方法被多个进程同时调用的情况。当这个方法中存在一些 非线程安全的代码时,方法重入会导致数据不一致的情况。Timer方法重入是指使用多线程计时器,一个Timer处理还没有完成,到了时间,另一 Timer还会继续进入该方法进行处理。下面演示一下重入问题的产生(可能重现的不是很好,不过也能简单一下说明问题了):

    复制代码
    //用来造成线程同步问题的静态成员
    private static int outPut = 1;
    //次数,timer没调一次方法自增1
    private static int num = 0;
     
    private static System.Timers.Timer timer = new System.Timers.Timer();
     
    public static void Main()
    {
        timer.Interval = 1000;
        timer.Elapsed += TimersTimerHandler;
        timer.Start();
     
        Console.WriteLine("按任意键退出程序。");
        Console.ReadLine();
    }
     
    /// <summary>
    /// System.Timers.Timer的回调方法
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private static void TimersTimerHandler(object sender, EventArgs args)
    {
        int t = ++num;
        Console.WriteLine(string.Format("线程{0}输出:{1},       输出时间:{2}", t, outPut.ToString(),DateTime.Now));
        System.Threading.Thread.Sleep(2000);
        outPut++;
        Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(),DateTime.Now));
    }
    复制代码

    下面显示一下输出结果:

    是不是感觉上面输出结果很奇怪,首先是线程1输出为1,没有问题,然后隔了2秒后线程1自增1后输出为2,这就有问题了,中间为什么还出现了线程2 的输出?更奇怪的是线程2刚开始输出为1,自增1后尽然变成了3!其实这就是重入所导致的问题。别急,咱们分析一下就知道其中的缘由了。

    首先timer启动计时后,开启一个线程1执行方法,当线程1第一次输出之后,这时线程1休眠了2秒,此时timer并没有闲着,因为设置的计时间 隔为1秒,当在线程1休眠了1秒后,timer又开启了线程2执行方法,线程2才不管线程1是执行中还是休眠状态,所以此时线程2的输出也为1,因为线程 1还在休眠状态,并没有自增。然后又隔了1秒,这时发生同时发生两个事件,线程1过了休眠状态自增输出为2,timer同时又开启一个线程3,线程3输出 的为线程1自增后的值2,又过了1秒,线程2过了休眠状态,之前的输出已经是2,所以自增后输出为3,又过了1秒……我都快晕了,大概就是这意思吧,我想 表达的就是:一个Timer开启的线程处理还没有完成,到了时间,另一Timer还会继续进入该方法进行处理。

    那怎么解决这个问题呢?解决方案有三种,下面一一道来,适应不同的场景,不过还是推荐最后一种,比较安全。

    解决方案

    1、使用lock(Object)的方法来防止重入,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就等待执行,适用重入很少出现的场景(具体也没研究过,可能比较占内存吧)。

    代码跟上面差不多,在触发的方法中加入lock,这样当线程2进入触发的方法中,发现已经被锁,会等待锁中的代码处理完在执行,代码如下:

    复制代码
    private static object locko = new object();  
     /// <summary>
     /// System.Timers.Timer的回调方法
     /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private static void TimersTimerHandler(object sender, EventArgs args)
    {
    int t = ++num;  lock (locko)   {
                Console.WriteLine(string.Format("线程{0}输出:{1},       输出时间:{2}", t, outPut.ToString(), DateTime.Now));
                System.Threading.Thread.Sleep(2000);
                outPut++;
                Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now));
            }
        }
    复制代码

    2、设置一个标志,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就放弃(注意这里是放弃,而不是等待哦,看看执行结果就明白啥意思了)执行,适用重入经常出现的场景。代码如下:

    复制代码
    private static int inTimer = 0; 
    /// <summary>
    /// System.Timers.Timer的回调方法
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private static void TimersTimerHandler(object sender, EventArgs args)
    {
        int t = ++num;
        if (inTimer == 0)
        {
            inTimer = 1;
            Console.WriteLine(string.Format("线程{0}输出:{1},       输出时间:{2}", t, outPut.ToString(), DateTime.Now));
            System.Threading.Thread.Sleep(2000);
            outPut++;
            Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now));
            inTimer = 0;
        }
    }
    复制代码

    3、在多线程下给inTimer赋值不够安全,Interlocked.Exchange提供了一种轻量级的线程安全的给对象赋值的方法(感觉比较高上 大,也是比较推荐的一种方法),执行结果与方法2一样,也是放弃执行。Interlocked.Exchange用法参考这里。

    复制代码
    private static int inTimer = 0; 
    /// <summary>
    /// System.Timers.Timer的回调方法
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private static void TimersTimerHandler(object sender, EventArgs args)
    {
        int t = ++num;
    //这个地方说明下,意思是判断inTimer是否为零,如果为零那么进入下面逻辑,然后inTimer的值变成1 if (Interlocked.Exchange(ref inTimer, 1) == 0) { Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now)); System.Threading.Thread.Sleep(2000); outPut++; Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now));
    //把inTimer的值改为0 Interlocked.Exchange(ref inTimer, 0); } }
    复制代码
  • 相关阅读:
    在非控制台程序中打印出printf
    如何将动态链接库(C++ DLL)中的printf显示在其被调用的程序控制台上
    Android NDK Build 参数
    查找包含××××××字符的文件名
    Ubuntu安装Fcitx(小企鹅五笔输入法)
    查询所有表的记录数SQLServer
    查询某个表或者所有表的字段说明 SQLServer
    安卓64位Ubuntu的32位包安装
    gen already exists but is not a source folder ZT
    mysql中查看datadir目录
  • 原文地址:https://www.cnblogs.com/soundcode/p/13705575.html
Copyright © 2020-2023  润新知