最近做一個小專案,專案中有一個定時服務,需要向對方定時傳送資料,時間間隔是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 }
從圖中可以看到,問題同樣得到解決,而且更簡單,精妙。
最後總結一下:解決問題的思維很重要,但是如果跳出思維站到更高的抽象層次上考慮問題貌似也很難得。。。