最近做一個小項目,項目中有一個定時服務,需要向對方定時發送數據,時間間隔是1.5s,然后就想到了用C#的Timer類,我們知道Timer
確實非常好用,因為里面有非常人性化的start和stop功能,在Timer里面還有一個Interval,就是用來設置時間間隔,然后時間間隔到了就會觸
發Elapsed事件,我們只需要把callback函數注冊到這個事件就可以了,如果Interval到了就會觸發Elapsed,貌似一切看起來很順其自然,但是
有一點一定要注意,callback函數本身執行也是需要時間的,也許這個時間是1s,2s或者更長時間,而timer類卻不管這些,它只顧1.5s觸發一下
Elapsed,這就導致了我的callback可能還沒有執行完,下一個callback又開始執行了,也就導致了沒有達到我預期的1.5s的效果,并且還出現了
一個非常嚴重的問題,那就是線程激增,非??植馈?/p>
下面舉個例子,為了簡化一下,我就定義一個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-10s34 Thread.Sleep(new Random().Next(8000, 10000));35 36 Console.WriteLine("當前時間:{0},定時數據發送成功!", DateTime.Now);37 }38 }39 40 class TimerCustom : System.Timers.Timer41 {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.Timer59 {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.Timer46 {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 }
從圖中可以看到,問題同樣得到解決,而且更簡單,精妙。
最后總結一下:解決問題的思維很重要,但是如果跳出思維站到更高的抽象層次上考慮問題貌似也很難得。。。
新聞熱點
疑難解答