C#中timer類的三種不同用法(含timer為什么只執(zhí)行了一次)
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
1、System.Windows.Forms.Timer 實現(xiàn)按用戶定義的時間間隔引發(fā)事件的計時器。此計時器最宜用于 Windows 窗體應用程序中,并且必須在窗口中使用。 2、System.Threading.Timer 提供以指定的時間間隔執(zhí)行方法的機制。無法繼承此類。 3、System.Timers.Timer 在應用程序中生成定期事件。 這三個定時器位于不同的命名空間內(nèi),上面大概介紹了3個定時器的用途,其中第一個是只能在Windows窗體中使用的控件。 在.NET1.1里面,第3個System.Timers.Timer,也是可以拖拽使用,而.NET2.0開始取消了,只能手動編寫代碼。而后2個沒有限制制。 下面通過具體的列子來看3個Timer的使用和區(qū)別 一 、System.Windows.Forms.Timer #region System.Windows.Forms.Timer public partial class Form1 : Form { public Form1() { InitializeComponent(); } int num = 0; private void Form_Timer_Tick(object sender, EventArgs e) { label1.Text = (++num).ToString(); Thread.Sleep(3000); } private void button1_Click(object sender, EventArgs e) { Form_Timer.Start(); } private void button2_Click(object sender, EventArgs e) { Form_Timer.Stop(); } } #endregion 上面這個是一個很簡單的功能,在Form窗體上拖了一個System.Windows.Forms.Timer控件名字為Form_Timer,在屬性窗中把Enable屬性設置為Ture,Interval是定時器的間隔時間。雙擊這個控件就可以看到 Form_Timer_Tick方法。 在這個方法中,我們讓她不停的加一個數(shù)字并顯示在窗體上,2個按鈕提供了對計時器的控制功能。執(zhí)行的時候你去點擊其他窗體在回來,你會發(fā)現(xiàn)我們的窗體失去響應了。 因為我們這里使用Thread.Sleep(3000); 讓當前線程掛起,而UI失去相應,說明了這里執(zhí)行時候采用的是單線程。也就是執(zhí)行定時器的線程就是UI線程。Timer 用于以用戶定義的事件間隔觸發(fā)事件。 Windows 計時器是為單線程環(huán)境設計的,其中,UI 線程用于執(zhí)行處理。它要求用戶代碼有一個可用的 UI 消息泵,而且總是在同一個線程中操作,或者將調(diào)用封送到另一個線程。 在Timer內(nèi)部定義的了一個Tick事件,我們前面雙擊這個控件時實際是增加了一行代碼。 this.Form_Timer.Tick += new System.EventHandler(this.Form_Timer_Tick); 然后Windows將這個定時器與調(diào)用線程關聯(lián)(UI線程)。當定時器觸發(fā)時,Windows把一個定時器消息插入到線程消息隊列中。調(diào)用線程執(zhí)行一個消息泵提取消息,然后發(fā)送到回調(diào)方法中(這里的Form_Timer_Tick方法)。 而這些都是單線程進行了,所以在執(zhí)行回調(diào)方法時UI會假死。所以使用這個控件不宜執(zhí)行計算受限或IO受限的代碼,因為這樣容易導致界面假死,而應該使用多線程調(diào)用的Timer。 另外要注意的是這個控件時間精度不高,精度限定為 55 毫秒。 二、System.Timers.Timer 接下來就看下另一個Timer,我們用它來改寫上面的程序 #region System.Windows.Forms.Timer public partial class Form1 : Form { public Form1() { InitializeComponent(); } int num = 0; DateTime time1 = new DateTime(); DateTime time2 = new DateTime(); //定義Timer System.Timers.Timer Timers_Timer = new System.Timers.Timer(); private void button1_Click(object sender, EventArgs e) { //手動設置Timer,開始執(zhí)行 Timers_Timer.Interval = 20; Timers_Timer.Enabled = true; Timers_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timers_Timer_Elapsed); time1 = DateTime.Now; } void Timers_Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { label1.Text = Convert.ToString((++num)); //顯示到lable Thread.Sleep(3000); } private void button2_Click(object sender, EventArgs e) { //停止執(zhí)行 Timers_Timer.Enabled = false; time2 = DateTime.Now; MessageBox.Show(Convert.ToString(time2 - time1)); } } #endregion 我們可以看到這個代碼和前面使用Form.Timer的基本相同,不同的是我們是手動定義的對象,而不是通過拉控件。他也有Interval ,Enabled 等屬性,作用和第一是一樣的。 不同的是他的事件名為Elapsed ,但是和上面的Tick一樣,綁定一個委托的方法。只是這里我們是手動完成的。另外不同之處是Form.Timer我們可以用Stop和Start方法控制,而這里是通過Enable屬性控制。 但實際上也可以用Stop和Start方法,內(nèi)部也是通過他自己的Enable來控制的。 最大的不同就是上面的代碼在調(diào)試時會報錯,提示你"線程間操作無效: 從不是創(chuàng)建控件“l(fā)abel1”的線程訪問它。"但如果你不調(diào)試直接運行是OK的,而且運行時你去拖動窗體會發(fā)現(xiàn)沒有出現(xiàn)假死。 從這里我們就可以知道這里的Timer的創(chuàng)建線程和執(zhí)行線程不是同一個線程。也就是使用了多線程。Timer的創(chuàng)建線程是UI線程,而執(zhí)行線程是TheardPool中的線程,所以不會假死,但調(diào)試的時候會報錯,因為非控件的創(chuàng)建線程不能操作控件。 但你可以直接運行,這里是VS05做了手腳。解決辦法很多,用delegate.BeginInvoke()等等。這里介紹特有的一種方法,設置Timer的SynchronizingObject屬性,Timers_Timer.SynchronizingObject = label1; 這樣調(diào)試運行時就不會報錯了,但是設置了這個屬性Timer就編程單線程調(diào)用了,就基本和第一個完全一樣了。 /// 在timer中使用SynchronizingObject的原因(tangtao_xp的注解) /// 請參考https://msdn.microsoft.com/en-us//library/system.timers.timer.synchronizingobject /// 1.如果SynchronizingObject為null,timer的Elapsed事件會默認由線程池進行處理 /// 2.如果timer的Elapsed事件要在windows Form等UI元件中處理,就會出現(xiàn)UI線程訪問線程池情況 /// 此時會引發(fā)錯誤和異常;將SynchronizingObject設置成要處理timer的Elapsed事件的UI元件, /// 該timer就會由該UI元件的線程創(chuàng)建,從而避免異常。 三 、System.Threading.Timer 繼續(xù)用這個對象改造程序。 #region System.Windows.Forms.Timer public partial class Form1 : Form { public Form1() { InitializeComponent(); } int num = 0; DateTime time1 = new DateTime(); DateTime time2 = new DateTime(); System.Threading.Timer Thread_Time; private void button1_Click(object sender, EventArgs e) { //啟動 Thread_Time = new System.Threading.Timer(Thread_Timer_Method, null, 0, 20); time1 = DateTime.Now; } void Thread_Timer_Method(object o) { label1.Text = Convert.ToString((++num)); System.Threading.Thread.Sleep(3000); } private void button2_Click(object sender, EventArgs e) { //停止 Thread_Time.Dispose(); time2 = DateTime.Now; MessageBox.Show(Convert.ToString(time2 - time1)); } } #endregion 用Threading.Timer時的方法,和前面就不太相同了,所以的參數(shù)全部在構(gòu)造函數(shù)中進行了設置,而且可以設置啟動時間。而且沒有提供start和stop方法來控制計時器。 而且是以一種回調(diào)方法的方式實現(xiàn),而不是通過事件來實現(xiàn)的。他們之間還是有區(qū)別的。我們只有銷毀掉對象來停止他。 當你運行時,你會發(fā)現(xiàn)他和前面的Timers.Timer一樣,是多線程的,主要表現(xiàn)在不會假死,調(diào)試運行報錯。但跟讓你奇怪的是,我們的代碼竟然無法讓她停止下來。 調(diào)用了Dispose方法沒有用。問題在那?然后有進行了測試,修改了間隔時間為100,200,500,1000,3000,4000。 這幾種情況。發(fā)現(xiàn)當間隔為500ms以上是基本馬上就停止了。而間隔時間相對執(zhí)行時間越短,繼續(xù)執(zhí)行的時間越長。這應該是在間隔時間小于執(zhí)行時間時多個線程運行造成的。因為所有的線程不是同時停止的。間隔越短,線程越多,所以執(zhí)行次數(shù)越多。 最后來看下這個對象另外一個特殊的地方。 static void Main() { Timer t = new Timer(Test, null, 0, 1000); Console.ReadLine(); } public static void Test(object o) { Console.WriteLine("hello world"); GC.Collect(); } 這段代碼會輸出什么結(jié)果呢?默認情況他只輸出一次,就停止了。為什么呢?根據(jù)上面說的,當定義對象t,執(zhí)行代碼后,進行了強制垃圾回收,因為t在Main中沒有其他引用,所以被回收掉了。 但是如果我們把編譯器的”優(yōu)化“項取消掉,在看看情況。程序進然一直在輸出。為什么執(zhí)行垃圾回收卻沒有被回收呢?因為這個禁用優(yōu)化選項,t的聲明周期被擴展到了方法結(jié)束。所以一直執(zhí)行。 因為編譯器默認是優(yōu)化的,所以我們必須保證Timer對象一直被引用,而避免被垃圾回收。所以我們可以在編譯器打開優(yōu)化的情況下,在Main函數(shù)最后加上t=null保證回收前被引用,但你發(fā)現(xiàn),這樣是沒用的。 因為JIT編譯器優(yōu)化后會吧t=null直接刪除,所以我們用t.Dispose(),就可以達到目的。在我們進行垃圾回收時,CLR發(fā)現(xiàn)t還有被引用,還沒執(zhí)行Dispose所以不會被回收。 是以Threading.Timer有時候會出現(xiàn)運行一次就停止或者是銷毀了還在運行的情況,而且和編譯器優(yōu)化也有關,所以使用時要注意。 最后看下MSDN的描述: 只要在使用 Timer,就必須保留對它的引用。對于任何托管對象,如果沒有對 Timer 的引用,計時器會被垃圾回收。即使 Timer 仍處在活動狀態(tài),也會被回收。當不再需要計時器時,請使用 Dispose 方法釋放計時器持有的資源。 如果希望在計時器被釋放時接收到信號,請使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重載。計時器已被釋放后,WaitHandle 便終止。 總結(jié): System.Threading.Timer 是一個簡單的輕量計時器,它使用回調(diào)方法并由線程池線程提供服務。不建議將其用于 Windows 窗體,因為其回調(diào)不在用戶界面線程上進行。System.Windows.Forms.Timer 是用于 Windows 窗體的更佳選擇。 要獲取基于服務器的計時器功能,可以考慮使用 System.Timers.Timer,它可以引發(fā)事件并具有其他功能。 在《CLR Via C#》中講多線程時有提到這3個計時器,但作者說System.Timers.Timer是對System.Threading.Timer的報裝,不推薦使用,但是在我的WEB項目中的Application_Start中我還是使用的這個而不是Threading.Timer,因為使用Threading.Timer時只執(zhí)行了一次就不在執(zhí)行了。 對于計時器在B/S結(jié)構(gòu)中的使用就復雜一些,一般我們把計時器放在Application_OnStart中,這樣全局維護一個計時器,可以進行定期備份數(shù)據(jù)庫,定期維護用戶等操作,而且方法寫作靜態(tài)的,以免被垃圾回收。 而不建議在一般的aspx頁面中使用,因為服務器端的定時器對用戶這樣意義不大,完全可以使用JS代替。而且這個頁面的每個請求都可能引入一個新的定時器,導致系統(tǒng)崩潰。 另外,定時器是ASP.NET進程,IIS有關,所以對用重要的執(zhí)行任務,還是建議寫成服務或獨立程序放在服務器上執(zhí)行好了。 System.Windows.Forms.Timer是應用于WinForm中的,它是通過Windows消息機制實現(xiàn)的,類似于VB或Delphi中的Timer控件,內(nèi)部使用API SetTimer實現(xiàn)的。它的主要缺點是計時不精確,而且必須有消息循環(huán),Console Application(控制臺應用程序)無法使用。 System.Timers.Timer和System.Threading.Timer非常類似,它們是通過.NET Thread Pool實現(xiàn)的,輕量,計時精確,對應用程序、消息沒有特別的要求。System.Timers.Timer還可以應用于WinForm,完全取代上面的Timer控件。它們的缺點是不支持直接的拖放,需要手工編碼。 該文章在 2024/1/3 22:24:26 編輯過 |
關鍵字查詢
相關文章
正在查詢... |