最近做一个小项目,项目中有一个定时服务,需要向对方定时发送数据,时间间隔是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。
1namespaceSample2{3classPRogram4{5staticvoidMain(string[] args)6{7TimerCustom timer =newTimerCustom();89timer.Interval =1500;1011timer.Elapsed += (obj, evt) =>12{13TimerCustom singleTimer = objasTimerCustom;1415if(singleTimer !=null)16{17if(singleTimer.queue.Count !=0)18{19varitem =singleTimer.queue.Dequeue();2021Send(item);22}23}24};2526timer.Start();2728Console.Read();29}3031staticvoidSend(intobj)32{33//随机暂定8-10s34Thread.Sleep(newRandom().Next(8000,10000));3536Console.WriteLine("当前时间:{0},定时数据发送成功!", DateTime.Now);37}38}3940classTimerCustom : System.Timers.Timer41{42publicQueue<int> queue =newQueue<int>();4344publicTimerCustom()45{46for(inti =0; i <short.MaxValue; i++)47{48queue.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再执行。
1namespaceSample2{3classProgram4{5staticvoidMain(string[] args)6{7TimerCustom timer =newTimerCustom();89timer.Interval =1500;1011timer.Elapsed += (obj, evt) =>12{13TimerCustom singleTimer = objasTimerCustom;1415if(singleTimer !=null)16{17//如果当前等待线程>2,就踢掉该线程18if(Interlocked.Read(refsingleTimer.lockNum) >2)19return;2021Interlocked.Increment(refsingleTimer.lockNum);2223//这里的lock只能存在一个线程等待24lock(singleTimer.lockMe)25{26if(!singleTimer.isFirst)27{28Thread.Sleep((int)singleTimer.Interval);29}3031singleTimer.isFirst =false;3233if(singleTimer.queue.Count !=0)34{35varitem =singleTimer.queue.Dequeue();3637Send(item);3839Interlocked.Decrement(refsingleTimer.lockNum);40}41}42}43};4445timer.Start();4647Console.Read();48}4950staticvoidSend(intobj)51{52Thread.Sleep(newRandom().Next(8000,10000));5354Console.WriteLine("当前时间:{0},邮件发送成功!", DateTime.Now);55}56}5758classTimerCustom : System.Timers.Timer59{60publicQueue<int> queue =newQueue<int>();6162publicobjectlockMe =newobject();6364publicboolisFirst =true;6566///<summary>67///为保持连贯性,默认锁住两个68///</summary>69publiclonglockNum =0;7071publicTimerCustom()72{73for(inti =0; i <short.MaxValue; i++)74{75queue.Enqueue(i);76}77}78}79}
从图中可以看到,已经没有同一秒出现重复任务的发送情况了,并且线程也给压制下去了,乍一看效果不是很明显,不过这是在一个任务的情况
下的场景,任务越多就越明显了,所以这个就达到我要的效果。
2. 从上面的解决方案来看,其实我们的思维已经被问题约束住了,当时我也是这样,毕竟坑出来了,就必须来填坑,既然在callback中出现线程
蜂拥的情况,我当然要想办法管制了,其实这也没什么错,等问题解决了再回头考虑下时,我们会发现文章开头说的Timer类有强大的Stop和
Start功能,所以。。。。这个时候思维就跳出来了,何不在callback执行的时候把Timer关掉,执行完callback后再把Timer开启,这样不就
可以解决问题吗?好吧,说干就干。
1namespaceSample2{3classProgram4{5staticvoidMain(string[] args)6{7TimerCustom timer =newTimerCustom();89timer.Interval =1500;1011timer.Elapsed += (obj, evt) =>12{13TimerCustom singleTimer = objasTimerCustom;1415//先停掉16singleTimer.Stop();1718if(singleTimer !=null)19{20if(singleTimer.queue.Count !=0)21{22varitem =singleTimer.queue.Dequeue();2324Send(item);2526//发送完成之后再开启27singleTimer.Start();28}29}30};3132timer.Start();3334Console.Read();35}3637staticvoidSend(intobj)38{39Thread.Sleep(newRandom().Next(8000,10000));4041Console.WriteLine("当前时间:{0},邮件发送成功!", DateTime.Now);42}43}4445classTimerCustom : System.Timers.Timer46{47publicQueue<int> queue =newQueue<int>();4849publicobjectlockMe =newobject();5051///<summary>52///为保持连贯性,默认锁住两个53///</summary>54publiclonglockNum =0;5556publicTimerCustom()57{58for(inti =0; i <short.MaxValue; i++)59{60queue.Enqueue(i);61}62}63}64}
从图中可以看到,问题同样得到解决,而且更简单,精妙。
最后总结一下:解决问题的思维很重要,但是如果跳出思维站到更高的抽象层次上考虑问题貌似也很难得。。。