1. 程式人生 > >.NET WinForm中的三種Timer(計時器)的區別和用法

.NET WinForm中的三種Timer(計時器)的區別和用法

.NET中的三種Timer(計時器)的區別和用法卡卡 發表於 2014/12/5 13:02:00 | 分類標籤: Timer 計時器 ASP.NET

NET中有3個不同的定時器。這3個定時器分別是:

1.實現按使用者定義的時間間隔引發事件的計時器。此計時器最宜用於 Windows 窗體應用程式中,並且必須在視窗中使用。
System.Windows.Forms.Timer

2.提供以指定的時間間隔執行方法的機制。無法繼承此類。
System.Threading.Timer

/3.在應用程式中生成定期事件。
System.Timers.Timer

一 System.Windows.Forms.Timer

  1. public partial class Form1 : Form  
  2. {  
  3. public Form1()  
  4. {  
  5. InitializeComponent();  
  6. }  
  7. int num = 0;  
  8. privatevoid Form_Timer_Tick(object sender, EventArgs e)  
  9. {  
  10. label1.Text = (++num).ToString();  
  11. Thread.Sleep(3000);  
  12. }  
  13. privatevoid button1_Click(object sender, EventArgs e)  
  14. {  
  15. Form_Timer.Start();  
  16. }  
  17. privatevoid button2_Click(object sender, EventArgs e)  
  18. {  
  19. Form_Timer.Stop();  
  20. }  
  21. }  

上面這個是一個很簡單的功能,在Form窗體上拖了一個System.Windows.Forms.Timer控制元件名字為Form_Timer,在屬性窗中把Enable屬性設定為Ture,Interval是定時器的間隔時間。雙擊這個控制元件就可以看到 Form_Timer_Tick方法。在這個方法中,我們讓她不停的加一個數字並顯示在窗體上,2個按鈕提供了對計時器的控制功能。


執行的時候你去點選其他窗體在回來,你會發現我們的窗體失去響應了。因為我們這裡使用Thread.Sleep(3000);讓當前執行緒掛起,而UI失去相應,說明了這裡執行時候採用的是單執行緒。也就是執行定時器的執行緒就是UI執行緒。
Timer 用於以使用者定義的事件間隔觸發事件。Windows 計時器是為單執行緒環境設計的,其中,UI 執行緒用於執行處理。它要求使用者程式碼有一個可用的 UI 訊息泵,而且總是在同一個執行緒中操作,或者將呼叫封送到另一個執行緒。
在Timer內部定義的了一個Tick事件,我們前面雙擊這個控制元件時實際是增加了一行程式碼

this.Form_Timer.Tick += new System.EventHandler(this.Form_Timer_Tick);
然後Windows將這個定時器與呼叫執行緒關聯(UI執行緒)。當定時器觸發時,Windows把一個定時器訊息插入到執行緒訊息佇列中。呼叫執行緒執行一個訊息泵提取訊息,然後傳送到回撥方法中(這裡的Form_Timer_Tick方法)。而這些都是單執行緒進行了,所以在執行回撥方法時UI會假死。所以使用這個控制元件不宜執行計算受限或IO受限的程式碼,因為這樣容易導致介面假死,而應該使用多執行緒呼叫的Timer。另外要注意的是這個控制元件時間精度不高,精度限定為 55 毫秒。我們把Interval設定20ms,然後在start和stop方法中記錄當前時,並計算出執行時間:

二 System.Timers.Timer


接下來就看下另一個Timer,我們用他來改寫上面的程式

  1. #region System.Windows.Forms.Timer  
  2. public partial class Form1 : Form  
  3. {  
  4. public Form1()  
  5. {  
  6. InitializeComponent();  
  7. }  
  8. int num = 0;  
  9. DateTime time1 =new DateTime();  
  10. DateTime time2 =new DateTime();  
  11. //定義Timer
  12. System.Timers.Timer Timers_Timer =new System.Timers.Timer();  
  13. privatevoid button1_Click(object sender, EventArgs e)  
  14. {  
  15. //手動設定Timer,開始執行
  16. Timers_Timer.Interval =20;  
  17. Timers_Timer.Enabled =true;  
  18. Timers_Timer.Elapsed +=new System.Timers.ElapsedEventHandler(Timers_Timer_Elapsed);  
  19. time1 = DateTime.Now;  
  20. }  
  21. void Timers_Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)  
  22. {  
  23. label1.Text = Convert.ToString((++num));//顯示到lable
  24. Thread.Sleep(3000);  
  25. }  
  26. privatevoid button2_Click(object sender, EventArgs e)  
  27. {  
  28. //停止執行
  29. Timers_Timer.Enabled =false;  
  30. time2 = DateTime.Now;  
  31. MessageBox.Show(Convert.ToString(time2-time1));  
  32. }  
  33. }  
  34. #endregion  


我們可以看到這個程式碼和前面使用Form.Timer的基本相同,不同的是我們是手動定義的物件,而不是通過拉控制元件。他也有Interval ,Enabled 等屬性,作用和第一是一樣的。不同的是他的事件名為Elapsed ,但是和上面的Tick一樣,繫結一個委託的方法。只是這裡我們是手動完成的。另外不同之處是Form.Timer我們可以用Stop和Start方法控制,而這裡是通過Enable屬性控制。但實際上也可以用Stop和Start方法,內部也是通過他自己的Enable來控制的。


最大的不同就是上面的程式碼在除錯時會報錯,提示你"執行緒間操作無效: 從不是建立控制元件“label1”的執行緒訪問它。"但如果你不除錯直接執行是OK的,而且執行時你去拖動窗體會發現沒有出現假死。從這裡我們就可以知道這裡的Timer的建立執行緒和執行執行緒不是同一個執行緒。也就是使用了多執行緒。Timer的建立執行緒是UI執行緒,而執行執行緒是TheardPool中的執行緒,所以不會假死,但除錯的時候會報錯,因為非控制元件的建立執行緒不能操作控制元件。但你可以直接執行,這裡是VS05做了手腳。解決辦法很多,用delegate.BeginInvoke()等等。這裡介紹特有的一種方法,設定Timer的SynchronizingObject屬性,Timers_Timer.SynchronizingObject = label1;這樣的話,我們的話,除錯執行時就不會報錯了,但是設定了這個屬性Timer就程式設計單執行緒呼叫了,就基本和第一個完全一樣了。
Timer 是為在多執行緒環境中用於輔助執行緒而設計的。伺服器計時器可以線上程間移動來處理引發的 Elapsed 事件,這樣就可以比 Windows 計時器更精確地按時引發事件。Elapsed 事件在 ThreadPool 執行緒上引發。如果 Elapsed 事件的處理時間比 Interval 長,在另一個 ThreadPool 執行緒上將會再次引發此事件。因此,事件處理程式應當是可重入的。


另外和前面不同的現象是每次加1後並沒有停止3秒在顯示。而是繼續顯示,只是速度稍慢。因為我們設定間隔為20ms,而執行時間為3s,所以會在20ms後在另一個執行緒中繼續執行,而當前執行緒被掛起而已。關於計時器的精度,取消3s的掛起,發現結果和第一個基本一致。
寫成如下就可以正常在lable中顯示遞增的數字,2s增加一次。

三 System.Threading.Timer


繼續用這個物件改造程式。

  1. public partial class Form1 : Form  
  2. {  
  3. public Form1()  
  4. {  
  5. InitializeComponent();  
  6. }  
  7. int num = 0;  
  8. DateTime time1 =new DateTime();  
  9. DateTime time2 =new DateTime();  
  10. System.Threading.Timer Thread_Time;  
  11. privatevoid button1_Click(object sender, EventArgs e)  
  12. {  
  13. //啟動
  14. Thread_Time =new System.Threading.Timer(Thread_Timer_Method,null,0,20);  
  15. time1 = DateTime.Now;  
  16. }  
  17. void Thread_Timer_Method(object o)  
  18. {  
  19. label1.Text = Convert.ToString((++num));  
  20. System.Threading.Thread.Sleep(3000);  
  21. }  
  22. privatevoid button2_Click(object sender, EventArgs e)  
  23. {  
  24. //停止
  25. Thread_Time.Dispose();  
  26. time2 = DateTime.Now;  
  27. MessageBox.Show(Convert.ToString(time2-time1));  
  28. }  
  29. }  

用Threading.Timer時的方法,和前面就不太相同了,所以的引數全部在建構函式中進行了設定,而且可以設定啟動時間。而且沒有提供start和stop方法來控制計時器。而且是以一種回撥方法的方式實現,而不是通過事件來實現的。他們之間還是有區別的。


我們只有銷燬掉物件來停止他。當你執行時,你會發現他和前面的Timers.Timer一樣,是多執行緒的,主要表現在不會假死,除錯執行報錯。但跟讓你奇怪的是,我們的程式碼竟然無法讓她停止下來。呼叫了Dispose方法沒有用。問題在那?然後有進行了測試,修改了間隔時間為100,200,500,1000,3000,4000。這幾種情況。發現當間隔為500ms以上是基本馬上就停止了。而間隔時間相對執行時間越短,繼續執行的時間越長。這應該是在間隔時間小於執行時間時多個執行緒執行造成的。因為所有的執行緒不是同時停止的。間隔越短,執行緒越多,所以執行次數越多。
最後來看下這個物件另外一個特殊的地方。

  1. staticvoid Main()  
  2. {  
  3. Timer t =new Timer(Test,null,0,1000);  
  4. Console.ReadLine();  
  5. }  
  6. publicstaticvoid Test(object o)  
  7. {  
  8. Console.WriteLine("nihao");  
  9. GC.Collect();  
  10. }  


這段程式碼會輸出什麼結果呢?預設情況他只輸出一次,就停止了。為什麼呢?根據上面說的,當定義物件t,執行程式碼後,進行了強制垃圾回收,因為t在Main中沒有其他引用,所以被回收掉了。但是如果我們吧編譯器的”優化“項取消掉,在看看情況。程式進然一直在輸出。為什麼執行垃圾回收卻沒有被回收呢?因為這個禁用優化選項,t的宣告週期被擴充套件到了方法結束。所以一直執行。


因為編譯器預設是優化的,所以我們必須保證Timer物件一直被引用,而避免被垃圾回收。所以我們可以在編譯器開啟優化的情況下,在Main函式最後加上t=null保證回收前被引用,但你發現,這樣是沒用的。因為JIT編譯器優化後會吧t=null直接刪除,所以我們用t.Dispose(),就可以達到目的。在我們進行垃圾回收時,CLR發現t還有被引用,還沒執行Dispose所以不會被回收。是以Threading.Timer有時候會出現執行一次就停止或者是銷燬了還在執行的情況,而且和編譯器優化也有關,所以使用時要注意。
另一個例子:

  1. using System; 
  2. using System.Threading; 
  3. namespace CallBacks 
  4.    class Program 
  5.     { 
  6.        private string message; 
  7.        privatestatic Timer timer; 
  8.        privatestatic bool complete; 
  9.        staticvoid Main(string[] args) 
  10.         { 
  11.             Program p =new Program(); 
  12.             Thread workerThread =new Thread(p.DoSomeWork); 
  13.             workerThread.Start(); 
  14.            //create timer with callback
  15.             TimerCallback timerCallBack = 
  16.                new TimerCallback(p.GetState); 
  17.             timer =new Timer(timerCallBack,null,  
  18.                 TimeSpan.Zero, TimeSpan.FromSeconds(2)); 
  19.            //wait for worker to complete
  20.            do
  21.             { 
  22.                //simply wait, do nothing
  23.             }while (!complete); 
  24.             Console.WriteLine("exiting main thread"); 
  25.             Console.ReadLine(); 
  26.         } 
  27.        publicvoid GetState(Object state) 
  28.         { 
  29.            //not done so return
  30.            if (message == string.Empty)return
  31.             Console.WriteLine("Worker is {0}", message); 
  32.            //is other thread completed yet, if so signal main
  33.            //thread to stop waiting
  34.            if (message =="Completed"
  35.             { 
  36.                 timer.Dispose(); 
  37.                 complete =true
  38.             } 
  39.         } 
  40.        publicvoid DoSomeWork() 
  41.         { 
  42.             message ="processing"
  43.            //simulate doing some work
  44.             Thread.Sleep(3000);  
  45.             message ="Completed"
  46.         } 
  47.     } 
  48. }  

總結:
System.Threading.Timer 是一個簡單的輕量計時器,它使用回撥方法並由執行緒池執行緒提供服務。不建議將其用於 Windows 窗體,因為其回撥不在使用者介面執行緒上進行。System.Windows.Forms.Timer 是用於 Windows 窗體的更佳選擇。要獲取基於伺服器的計時器功能,可以考慮使用 System.Timers.Timer,它可以引發事件並具有其他功能。


在《CLR Via C#》中講多執行緒時有提到這3個計時器,但作者說System.Timers.Timer是對System.Threading.Timer的報裝,不推薦使用,但是在我的WEB專案中的Application_Start中我還是使用的這個而不是Threading.Timer,因為使用Threading.Timer時只執行了一次就不在執行了。


對於計時器在B/S結構中的使用就複雜一些,一般我們把計時器放在Application_OnStart中,這樣全域性維護一個計時器,可以進行定期備份資料庫,定期維護使用者等操作,而且方法寫作靜態的,以免被垃圾回收。而不建議在一般的aspx頁面中使用,因為伺服器端的定時器對使用者這樣意義不大,完全可以使用JS代替。而且這個頁面的每個請求都可能引入一個新的定時器,導致系統崩潰。另外,定時器是ASP.NET程序,IIS有關,所以對用重要的執行任務,還是建議寫成服務或獨立程式放在伺服器上執行好了。