• 【转】最近用Timer踩了一个坑,分享一下避免别人继续踩


    【转】最近用Timer踩了一个坑,分享一下避免别人继续踩

      最近做一个小项目,项目中有一个定时服务,需要向对方定时发送数据,时间间隔是1.5s,然后就想到了用C#的Timer类,我们知道Timer

    确实非常好用,因为里面有非常人性化的start和stop功能,在Timer里面还有一个Interval,就是用来设置时间间隔,然后时间间隔到了就会触

    发Elapsed事件,我们只需要把callback函数注册到这个事件就可以了,如果Interval到了就会触发Elapsed,貌似一切看起来很顺其自然,但是

    有一点一定要注意,callback函数本身执行也是需要时间的,也许这个时间是1s,2s或者更长时间,而timer类却不管这些,它只顾1.5s触发一下

    Elapsed,这就导致了我的callback可能还没有执行完,下一个callback又开始执行了,也就导致了没有达到我预期的1.5s的效果,并且还出现了

    一个非常严重的问题,那就是线程激增,非常恐怖。

       下面举个例子,为了简化一下,我就定义一个task任务,当然项目中是多个task任务一起跑的。

    一:问题产生

       为了具有更高的灵活性,我定义了一个CustomTimer类继承自Timer,然后里面可以放些Task要跑的数据,这里就定义一个Queue。

     1 namespace Sample
     2 {
     3     class Program
     4     {
     5         static void Main(string[] args)
     6         {
     7             TimerCustom timer = new TimerCustom();
     8 
     9             timer.Interval = 1500;
    10 
    11             timer.Elapsed += (obj, evt) =>
    12             {
    13                 TimerCustom singleTimer = obj as TimerCustom;
    14 
    15                 if (singleTimer != null)
    16                 {
    17                     if (singleTimer.queue.Count != 0)
    18                     {
    19                         var item = singleTimer.queue.Dequeue();
    20 
    21                         Send(item);
    22                     }
    23                 }
    24             };
    25 
    26             timer.Start();
    27 
    28             Console.Read();
    29         }
    30 
    31         static void Send(int obj)
    32         {
    33             //随机暂定8-10s
    34             Thread.Sleep(new Random().Next(8000, 10000));
    35 
    36             Console.WriteLine("当前时间:{0},定时数据发送成功!", DateTime.Now);
    37         }
    38     }
    39 
    40     class TimerCustom : System.Timers.Timer
    41     {
    42         public Queue<int> queue = new Queue<int>();
    43 
    44         public TimerCustom()
    45         {
    46             for (int i = 0; i < short.MaxValue; i++)
    47             {
    48                 queue.Enqueue(i);
    49             }
    50         }
    51     }
    52 }

    二:解决方法

    1.  从上图看,在一个任务的情况下就已经有14个线程了,并且在21s的时候有两个线程同时执行了,我的第一反应就是想怎么把后续执行callback的

    线程踢出去,也就是保证当前仅让两个线程在用callback,一个在执行,一个在等待执行,如果第一个线程的callback没有执行完,后续如果来了第三

    个线程的话,我就把这第三个线程直接踢出去,直到第一个callback执行完后,才允许第三个线程进来并等待执行callback,然后曾今的第二个线程开

    始执行callback,后续的就以此类推。。。

    然后我就想到了用lock机制,在customTimer中增加lockMe,lockNum,isFirst字段,用lockMe来锁住,用lockNum来踢当前多余的要执行callback

    的线程,用isFirst来判断是不是第一次执行该callback,后续callback的线程必须先等待1.5s再执行。

     1 namespace Sample
     2 {
     3     class Program
     4     {
     5         static void Main(string[] args)
     6         {
     7             TimerCustom timer = new TimerCustom();
     8 
     9             timer.Interval = 1500;
    10 
    11             timer.Elapsed += (obj, evt) =>
    12             {
    13                 TimerCustom singleTimer = obj as TimerCustom;
    14 
    15                 if (singleTimer != null)
    16                 {
    17                     //如果当前等待线程>2,就踢掉该线程
    18                     if (Interlocked.Read(ref singleTimer.lockNum) > 2)
    19                         return;
    20 
    21                     Interlocked.Increment(ref singleTimer.lockNum);
    22 
    23                     //这里的lock只能存在一个线程等待
    24                     lock (singleTimer.lockMe)
    25                     {
    26                         if (!singleTimer.isFirst)
    27                         {
    28                             Thread.Sleep((int)singleTimer.Interval);
    29                         }
    30 
    31                         singleTimer.isFirst = false;
    32 
    33                         if (singleTimer.queue.Count != 0)
    34                         {
    35                             var item = singleTimer.queue.Dequeue();
    36 
    37                             Send(item);
    38 
    39                             Interlocked.Decrement(ref singleTimer.lockNum);
    40                         }
    41                     }
    42                 }
    43             };
    44 
    45             timer.Start();
    46 
    47             Console.Read();
    48         }
    49 
    50         static void Send(int obj)
    51         {
    52             Thread.Sleep(new Random().Next(8000, 10000));
    53 
    54             Console.WriteLine("当前时间:{0},邮件发送成功!", DateTime.Now);
    55         }
    56     }
    57 
    58     class TimerCustom : System.Timers.Timer
    59     {
    60         public Queue<int> queue = new Queue<int>();
    61 
    62         public object lockMe = new object();
    63 
    64         public bool isFirst = true;
    65 
    66         /// <summary>
    67         /// 为保持连贯性,默认锁住两个
    68         /// </summary>
    69         public long lockNum = 0;
    70 
    71         public TimerCustom()
    72         {
    73             for (int i = 0; i < short.MaxValue; i++)
    74             {
    75                 queue.Enqueue(i);
    76             }
    77         }
    78     }
    79 }

    从图中可以看到,已经没有同一秒出现重复任务的发送情况了,并且线程也给压制下去了,乍一看效果不是很明显,不过这是在一个任务的情况

    下的场景,任务越多就越明显了,所以这个就达到我要的效果。

    2. 从上面的解决方案来看,其实我们的思维已经被问题约束住了,当时我也是这样,毕竟坑出来了,就必须来填坑,既然在callback中出现线程

      蜂拥的情况,我当然要想办法管制了,其实这也没什么错,等问题解决了再回头考虑下时,我们会发现文章开头说的Timer类有强大的Stop和

       Start功能,所以。。。。这个时候思维就跳出来了,何不在callback执行的时候把Timer关掉,执行完callback后再把Timer开启,这样不就

       可以解决问题吗?好吧,说干就干。

     1 namespace Sample
     2 {
     3     class Program
     4     {
     5         static void Main(string[] args)
     6         {
     7             TimerCustom timer = new TimerCustom();
     8 
     9             timer.Interval = 1500;
    10 
    11             timer.Elapsed += (obj, evt) =>
    12             {
    13                 TimerCustom singleTimer = obj as TimerCustom;
    14 
    15                 //先停掉
    16                 singleTimer.Stop();
    17 
    18                 if (singleTimer != null)
    19                 {
    20                     if (singleTimer.queue.Count != 0)
    21                     {
    22                         var item = singleTimer.queue.Dequeue();
    23 
    24                         Send(item);
    25 
    26                         //发送完成之后再开启
    27                         singleTimer.Start();
    28                     }
    29                 }
    30             };
    31 
    32             timer.Start();
    33 
    34             Console.Read();
    35         }
    36 
    37         static void Send(int obj)
    38         {
    39             Thread.Sleep(new Random().Next(8000, 10000));
    40 
    41             Console.WriteLine("当前时间:{0},邮件发送成功!", DateTime.Now);
    42         }
    43     }
    44 
    45     class TimerCustom : System.Timers.Timer
    46     {
    47         public Queue<int> queue = new Queue<int>();
    48 
    49         public object lockMe = new object();
    50 
    51         /// <summary>
    52         /// 为保持连贯性,默认锁住两个
    53         /// </summary>
    54         public long lockNum = 0;
    55 
    56         public TimerCustom()
    57         {
    58             for (int i = 0; i < short.MaxValue; i++)
    59             {
    60                 queue.Enqueue(i);
    61             }
    62         }
    63     }
    64 }

    从图中可以看到,问题同样得到解决,而且更简单,精妙。


    最后总结一下:解决问题的思维很重要,但是如果跳出思维站到更高的抽象层次上考虑问题貌似也很难得。。。

  • 相关阅读:
    aws-rds for mysql 5.7.34搭建备库
    Redis 未授权访问漏洞利用总结(转)
    mongoexport/mongimport命令详解
    mongodump/mongorestore命令详解
    redis stream类型 常用命令
    system_time_zone参数值由来
    MySQL加密解密函数AES_ENCRYPT AES_DECRYPT
    MySQL开启SSL加密
    MDL锁获取顺序和优先先
    explicit_defaults_for_timestamp 参数说明
  • 原文地址:https://www.cnblogs.com/RYouHoo-923/p/8274277.html
Copyright © 2020-2023  润新知