概要
在客戶端程序和服務器組件(包括windows服務)中,timer(定時器)通常扮演著一個重要角色。編寫高效的timer驅動的托管代碼,需要對程序流程和.net線程模型的精妙有清晰的理解。.NET Framework 類庫提供了三個不同的timer類:System.Windows.Forms.Timer, System,Timers.Timer 和 System.Threading.Timer。每個Timer類被設計優化用於不同的場合。本文研究了這三個Timer類,幫組你理解如何及何時該使用哪個類。
目錄
System.Windows.Forms.Timer
System.Timers.Timer
System.Threading.Timer
定時器的線程安全編程
處理timer事件的重入
結論
Microsoft® Windows® 中的Timer對象在行為發生時允許你進行控制。Timer最常見的一些用法是有規律的定時啟動一個進程,設置事件發生的間隔,在處理圖像時維持一致的動畫速度(不管處理器的速度如何)。在過去,對於使用Visual Basic®的開發人員來說, Timer甚至能用來模擬多任務。
如你所想,微軟.NET Framework為你提供了處理這些任務所需的工具。在.NET Framework類庫中有三個不同的Timer類:System.Windows.Forms.Timer, System,Timers.Timer 和 System.Threading.Timer。前兩個類出現在Visual Studio® .NET 工具箱中,你可以直接把它們拖拽到Windows窗體設計器或組件設計器。如果你不小心,這時麻煩就開始了。
Visual Studio .NET 工具箱在Windows窗體頁和組件頁都有一個Timer控件(見圖1)。很容易就用錯了,或者更糟的是沒有認識到它們是不同的。僅當目標是Windows窗體設計器時,使用Windows窗體頁上的Timer控件。這個控件會在你的窗體上放置一個System.Windows.Forms.Timer類的實例。正如工具箱中的其它控件,你可以讓Visual Studio .NET自動生成,或者也可以自己手工實例化、初始化這個類。
圖1 定時器控件
組件頁上的Timer控件可以安全地用於任何類。這個控件會創建System.Timers.Timer類的實例。如果你使用Visual Studio .NET 工具箱,無論是在Windows窗體設計器,還是在組件設計器,你都可以安全的使用這個Timer。當你處理一個繼承自System.ComponentModel.Component的類(例如處理Windows服務)時,Visual Studio .NET會使用組件設計器。System.Threading.Timer類不在Visual Studio .NET 工具箱中。它稍微復雜些,但也提供了更高級的控制,稍後你將在本文看到。
我們首先研究一下System.Windows.Forms.Timer和System.Timers.Timer。這兩個類有非常相似的對象模型。一會兒我會探究更高級的Sytem.Threading.Timer類。圖2是我在這篇文章中引用的例子程序的屏幕截圖。這個程序將幫助你更清晰的認識這幾個類。你可以從文章上方的鏈接下載完整代碼,並用它做試驗。
圖2 例子程序
System.Windows.Forms.Timer
如果你在找一個節拍器,你就找錯地方了。這個Timer類觸發的定時器事件與你的Windows窗體應用程序的其余代碼是同步的。這就是說,正在執行的應用程序代碼永遠也不會被這個Timer類的實例搶占(假定你沒有調用Application.DoEvents)。就像一個典型的Windows窗體應用程序的其余代碼一樣,這種Timer類的Timer事件處理器中的任何代碼都是使用應用程序的UI線程來執行。在空閒時間,UI線程也負責處理應用程序的Windows消息隊列中的所有消息,其中包括Windows API消息,也包括這種Timer類觸發的Tick事件。當應用程序沒有忙於做其他事時,UI線程就處理這些消息。
如果你在Visual Studio .NET之前,寫過VB代碼,你可能知道在基於Windows的應用程序中,允許UI線程在執行事件處理器時響應Windows消息的唯一方法,就是調用Application.DoEvents方法。正如VB中一樣,在.NET Framework中調用Application.DoEvents會導致一些問題。Application.DoEvents移交控制給UI消息泵,允許對所有未處理的事件進行處理。這會改變我剛才提到的程序執行路徑。如果在你代碼裡調用Application.DoEvents,你的程序流程會被中斷,以便處理這個Timer類的實例所觸發的定時器事件。這會導致不可預料的行為,使調試變得困難。
當我們執行例子程序,這個Timer類的行為就明顯了。點擊例子程序的Start按鈕,然後點擊Sleep按鈕,最後點擊Stop按鈕,將會產生下面的輸出:
System.Windows.Forms.Timer Started @ 4:09:28 PM
--> Timer Event 1 @ 4:09:29 PM on Thread: UIThread
--> Timer Event 2 @ 4:09:30 PM on Thread: UIThread
--> Timer Event 3 @ 4:09:31 PM on Thread: UIThread
Sleeping for 5000 ms...
--> Timer Event 4 @ 4:09:36 PM on Thread: UIThread
System.Windows.Forms.Timer Stopped @ 4:09:37 PM
例子程序把System.Windows.Forms.Timer類的Interval屬性設置為1000毫秒。正如你看到的,如果在主UI線程休眠(5秒)時,timer事件處理器繼續捕獲timer事件,那麼一旦UI線程再次被喚醒時,就應該顯示5個timer事件——UI線程休眠時每秒鐘一個。然而,在UI線程休眠時,timer處於掛起狀態。
用System.Windows.Forms.Timer編程已經夠簡單了——它有一個非常簡單直觀的編程接口。Start和Stop方法提供了一個設置Enable屬性(對Win32® SetTimer/ KillTimer 函數的輕量級封裝)的替代方法。剛才提到的Interval屬性,是不言自明的。盡管技術上,你可以把Interval屬性設置得低到一毫秒,但你應該知道.NET Framework文檔中說這個屬性只能精確到大約55毫秒(假設UI線程可用於處理)。
捕獲System.Windows.Forms.Timer類的實例觸發的事件,是通過把Tick事件關聯到標准的EventHandler代理來實現的,如下面例子中的代碼片段所示:
System.Windows.Forms.Timer tmrWindowsFormsTimer = new
System.Windows.Forms.Timer();
tmrWindowsFormsTimer.Interval = 1000;
tmrWindowsFormsTimer.Tick += new
EventHandler(tmrWindowsFormsTimer_Tick);
tmrWindowsFormsTimer.Start();
...
private void tmrWindowsFormsTimer_Tick(object sender,
System.EventArgs e) {
//Do something on the UI thread...
}
System.Timers.Timer
.NET Framework文檔之處System.Timers.Timer是一個基於服務器的定時器,是為多線程環境進行設計和優化的。這個Timer類的實例可以從多線程中安全的訪問。不像System.Windows.Forms.Timer,System.Timers.Timer類默認會從公共語言運行時的線程池獲取一個工作線程(worker thread)來調用你的timer事件處理器。這意味著你的Elapsed事件處理器中的代碼必須遵守Win32編程的黃金規則:控件的實例絕不能被除實例化它的線程以外的任何其他線程訪問。
System.Timers.Timer類提供了一個簡單的方式處理這樣的困境——它暴露了一個公有的SynchronizingObject屬性。把這個屬性設置成Windows窗體的一個實例(或Windows窗體上的一個控件),可以保證你的Elapsed事件處理器中的代碼運行在SynchronizingObject 被實例化的同一個線程。
如果你使用Visual Studio .NET工具箱,Visual Studio .NET會自動把SynchronizingObject屬性設置為當前窗體。起初可能看起來,使用有SynchronizingObject屬性的這個Timer類,使其在功能上與使用System.Windows.Forms.Timer等同。對於大部分功能,確實是這樣。當操作系統通知System.Timers.Timer類啟用的定時時間已過,定時器使用SynchronizingObject.BeginInvoke方法在創建SynchronizingObject的底層handle的線程上執行Elapsed事件代理。事件處理器會被阻塞,直到UI線程能處理它。然而,不像 System.Windows.Forms.Timer,事件最終還是會被觸發。就像你在圖2看到的,當UI線程不能處理時, System.Windows.Forms.Timer不會觸發事件。而System.Timers.Timer會把事件排到隊列中,等待UI線程可用時進行處理。
圖3顯示了如何使用SynchronizingObject 屬性。你可以使用例子程序分析這個類,選擇 System.Timers.Timer 單選按鈕,按照執行System.Windows.Forms.Timer同樣的順序執行這個類。這樣做會產生圖4所示輸出。
圖3 使用SynchronizingObject屬性
System.Timers.Timer tmrTimersTimer = new System.Timers.Timer();
tmrTimersTimer.Interval = 1000;
tmrTimersTimer.Elapsed += new
ElapsedEventHandler(tmrTimersTimer_Elapsed);
tmrTimersTimer.SynchronizingObject = this; //Synchronize with
//the current form...
tmrTimersTimer.Start();
……
private void tmrTimersTimer_Elapsed(object sender,
System.Timers.ElapsedEventArgs e) {
// Do something on the UI thread (same thread the form was
// created on)...
// If we didnt set SynchronizingObject we would be on a
// worker thread...
}