不論在用戶端應用程式還是伺服器元件(包括視窗服務)定時器通常扮演一個重要的角色。寫一個高效的定時器驅動型可管理代碼要求對程式流程有一個清晰的了解及掌握.NET線程模型的精妙之處。.NET架構類庫提供了三種不同的定時器類:System.Windows.Forms.Timer, System.Timers.Timer, 和System.Threading.Timer。每個類為不同的場合進行設計和優化。本文章将研究這三個類并讓你了解如何及何時應該使用哪一個類。
Microsoft? Windows?裡的定時器對象當行為發生時允許你進行控制。定時器一些最常用的地方就是有規律的定時啟動一個程序,在事件之間設定間隔,及當進行 圖形工作時維護固定的動畫速度(而不管處理函數的速度)。在過去,對于使用Visual Basic?的開發者來說,定時器甚至用來模拟多任務。
正如你所期望的那樣,對于你需要應對的不同場合微軟為你裝備了一些工具。在.NET架構類庫中有三種不同的定時器類:System.Windows.Forms.Timer,System.Timers.Timer,和System.Threading.Timer。頭兩個類出現在Visual Studio? .NET的工具箱視窗,這兩個定時器控件都允許你直接把它們拖拽到Windows窗體設計器或元件類設計器上。如果你不小心,這就是麻煩的開始。
Visual Studio .NET工具箱上的Windows窗體頁群組件頁(見Figure 1)都有定時器控件。非常容易的錯誤地使用它們當中的一個,或者更糟糕的是,根本意識不到它們的不同。僅當目标是Windows窗體設計器時才使用Windows窗體頁上的定時器控件。這個控件将在你的窗體上放置一個Systems.Windows.Forms.Timer類的執行個體。像工具箱上的其它控件一樣,你可以讓Visual Studio .NET處理其生成或者你自己手動的執行個體和初始化這個類。
Figure 1 定時器控件
在元件頁上的定時器控件可以被安全的用在任何類中。這個控件建立了一個System.Timers.Timer類的執行個體。如果你正在使用Visual Studio .NET工具箱,無論是Windows窗體設計器還是元件類設計器你都可以安全的使用這個類。在Visual Studio .NET中當你設計一個派生于System.ComponentModel.Component的類時使用元件類設計器。System.Threading.Timer類不出現在Visual Studio .NET工具箱視窗上。它稍微有點複雜但提供了一個更進階别的控件,稍後你會在本文章中看到。
Figure 2 例子程式
讓我們首先研究System.Windows.Forms.Timer和System.Timers.Timer類。這兩個類有着非常相似的對象模型。稍後我将探索更加進階的System.Threading.Timer類。Figure 2 是我将在整個文章引用的例子程式的一個螢幕快照。這個應用程式将會讓你獲得對這幾個定時器類的清晰的了解。你可以從本文章的開始連結處下載下傳完整的代碼并試驗它。
System.Windows.Forms.Timer
如果你在找一個節拍器,你已經走錯了地方了。這個定時器類引發的定時器事件是同你的視窗應用程式的其餘代碼相同步的。這意味着正在執行的代碼從來不會被這個定時器類的執行個體所搶占(假設你不調用Application.DoEvents)。就像一個典型窗體程式裡的其它代碼一樣,任何駐留在一個定時器事件處理函數(指的是該類型的定時器類)中的代碼也是使用應用程式的UI線程所執行。在空閑時候,該UI線程同樣要對應用程式的窗體消息隊列中的所有消息進行負責。這不僅包括由這個定時類引發的消息,也包括窗體API消息。無論何時你的程式不忙于做其它事情時該UI線程就處理這些消息。
在Visual Studio .NET之前如果你寫過Visual Basic代碼,你可能知道在一個視窗應用程式裡當正在執行一個事件處理函數時讓你的UI線程去響應其它窗體消息的唯一方法就是調用Application.DoEvents方法。就像Visual Basic一樣,從.NET架構中調用Application.DoEvents能夠産生許多問題。Application.DoEvents産生了對UI消息泵的控制,讓你對所有未處理的事件進行處理。這能夠改變我剛才提到的所期望的執行路徑。如果為了處理由該定時器類産生的定時器事件而在你的代碼中有一個Application.DoEvents的調用,你的程式流程可能會被打斷。這會産生不希望的行為并使調試困難。
運作例子程式就會使這個定時器類的行為變得清楚。單擊程式的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: UIThreadSleeping for 5000 ms...--> Timer Event 4 @ 4:09:36 PM on Thread: UIThreadSystem.Windows.Forms.Timer Stopped @ 4:09:37 PM |
例子程式設定System.Windows.Forms.Timer類的間隔屬性為1000毫秒。正如你所看到的,當UI線程正在睡眠(5秒)期間如果定時器事件處理函數仍然繼續捕捉定時器事件的話,當睡眠線程再次被喚醒的時候應該有5個定時器事件被顯示——在UI線程睡眠時每秒鐘一個。然而,當UI線程在睡眠時定時器卻保持挂起狀态。
對System.Windows.Forms.Timer的程式設計不能再簡單了——它有一個非常簡單和可直接程式設計的接口。Start和Stop方法實際上提供了一個設定使能屬性的改變方法(其本身是對Win32?的SetTimer和KillTimer功能的一個包裝)。我剛才提到的間隔屬性,名字本身就說明了問題。即使技術上你可以設定間隔屬性低到1毫秒,但你應該知道在.NET架構文檔中指出這個屬性大約精确到55毫秒(假定UI線程對于處理是可用的)。
捕捉由System.Windows.Forms.Timer類執行個體引發的事件是通過感覺一個标準的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架構文檔指出System.Timers.Timer類是一個伺服器定時器,是為多線程環境進行設計和優化。該定時器類的執行個體能夠被多個線程安全地通路。不像System.Windows.Forms.Timer,System.Timers.Timer預設的,将在一個工作者線程上調用你的定時器事件處理函數,該工作者線程是從公共語言運作時(CLR)線程池中獲得。這意味着在你的逝去的時間處理函數代碼中必須遵從Win32程式設計的黃金規則:除了建立該控件執行個體的線程之外,一個控件的執行個體從來不被任何其它的線程所通路。
System.Timers.Timer提供了一個簡單的方法處理這樣的困境——暴露一個公共的SynchronizingObject屬性。把該屬性設定為一個窗體執行個體(或者窗體上的一個控件)将保證你的事件處理函數代碼運作在SynchronizingObject被執行個體化的同一個線程裡。
如果你使用了Visual Studio .NET工具箱,Visual Studio .NET自動的設定SynchronizingObject屬性為目前的窗體執行個體。首先它設定該定時器的SynchronizingObject屬性使其在功能上同System.Windows.Forms.Timer類一樣。對于大部分功能,的确是這樣。當作業系統通知System.Timers.Timer類所允許的定時時間已過去,定時器使用SynchronizingObject.Begin.Invoke方法在一個線程上去執行事件委托,該線程是建立SynchronizingObject的線程。事件處理函數将被阻塞直到UI線程能夠處理它。然而不像System.Windows.Forms.Timer類一樣,該事件最終仍然能夠被引發。像你在Figure 2中看到的,當UI線程不能夠處理時System.Windows.Forms.Timer不會引發事件,可是當UI線程可用時System.Timers.Timer卻會排隊等候處理。
Figure 3是如何使用SynchronizingObject屬性的例子。使用例子程式并通過選擇System.Timers.Timer的radio按鈕你可以分析這個類,并按照執行System.Windows.Forms.Timer類行為的同樣順序運作該類,這樣就會産生Figure 4的輸出結果。
正如你所看到的,它不會跳過一個跳動——即使UI線程在睡眠。在每一個事件間隔就有一個時間消失事件處理會被排隊執行。因為UI線程在睡眠,是以當UI線程一旦被喚醒例子程式就會列出5個定時器事件(4到8)并能夠處理處理函數。
正如我早先提到的,System.Timers.Timer類成員非常類似與System.Windows.Forms.Timer。最大的差別就在與System.Timers.Timer類是對Win32可等待定時對象的一個包裝,并在工作者線程上産生一個時間片消失事件而不是在UI線程上産生一個時間标記事件。時間片消失事件必須與一個同ElapsedEventHandler委托像比對的事件處理函數相連接配接。事件處理函數接受一個ElapsedEventArgs類型的參數。
除了标準的EventArgs成員,ElapsedEventArgs類暴露了一個公共的SignalTime屬性,它包含了一個精确的定時器時間片消失的時間。因為這個類支援不同線程的通路,除了時間消失事件所在的線程,應該相信它的Stop方法能夠被其它線程所調用。這會潛在的導緻消失事件被引發即使其Stop方法已經被調用。你可以把SignalTime和Stop方法調用的時間進行比較來解決這個問題。
System.Timers.Timer也提供了AutoReset屬性來決定當時間片消失事件引發後是繼續進行還是隻這一次。要記住在定時器開始後重設間隔屬性會導緻目前計數為0。比如,設定了一個5秒的間隔,在間隔被改變為10秒時3秒已經過去了,那麼下一個定時器事件将會在上一個定時器事件13秒後發生。
System.Threading.Timer
第三個定時器類來自System.Threading名字空間。我願意說這是所有定時器類中最好的一個,但這會引起誤導。舉一個例子,我驚訝的發現對于駐留在System.Threading名字空間的這個類天生就不是線程安全的。(很明顯,這不意味着它不能以線程安全的方式使用)。這個類的可程式設計接口同其它兩個類也不一緻,它稍微有點麻煩。
不像我開始描述的兩個定時器類,System.Threading.Timer有四個重載構造函數,就像下面這樣:
以下為引用的内容: public Timer(TimerCallback callback, object state, long dueTime, long period); public Timer(TimerCallback callback, object state, UInt32 dueTime, UInt32 period); public Timer(TimerCallback callback, object state, int dueTime, int period); public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period); |
第一個參數(callback)要求一個TimerCallback的委托,它指向一個方法,該方法具有下面的結構:
public void TimerCallback(object state);
第二個參數(state)可以為空或者是包含程式規範資訊的對象。在每一個定時器事件被調用時該state對象作為一個參數傳遞給你的定時回調函數。記住定時回調功能是在一個工作者線程上執行的,是以你必須確定通路state對象的線程安全。
第三個參數(dueTime)讓你定義一個引發初始定時器事件的時間。你可指定一個0立即開始定時器或者阻止定時器自動的開始,你可以使用System.Threading.Timeout.Infinite常量。
第四個參數(period)讓你定義一個回調函數被調用的時間間隔(毫秒)。給該參數定義一個0或者Timeout.Infinite可以阻止後續的定時器事件調用。
一旦構造函數被調用,你仍然可以通過Change方法改變dueTime和period。該方法有下面四種重載形式:
以下為引用的内容: public bool Change(int dueTime, int period);public bool Change(uint dueTime, uint period);public bool Change(long dueTime, long period);public bool Change(TimeSpan dueTime, TimeSpan period); |
下面是我在例子程式中用到的開始和停止該定時器的代碼:
以下為引用的内容: //Initialize the timer to not start automatically...System.Threading.Timer tmrThreadingTimer = newSystem.Threading.Timer(new TimerCallback(tmrThreadingTimer_TimerCallback), null, System.Threading.Timeout.Infinite, 1000); //Manually start the timer...tmrThreadingTimer.Change(0, 1000); //Manually stop the timer...tmrThreadingTimer.Change(Timeout.Infinte, Timeout.Infinite); |
正如你所期望的那樣,通過選擇System.Threading.Timer類運作例子程式會産生同你看到的System.Timers.Timer類一樣的輸出結果。因為TimerCallback功能也是在工作者線程上被調用,沒有一個跳動被跳過(假設有工作者線程可用)。Figure 5顯示了例子程式的輸出結果。
不像System.Timers.Timer類,沒有與SynchronizingObject相對應的屬性被提供。任何請求通路UI控件的操作都必須通過控件的Invoke或BeginInvoke方法被列集
定時器的線程安全程式設計
為了最大限度的代碼重用,三種不同類型的定時器事件都調用了同樣的ShowTimerEventFired方法,下面就是三個定時器事件的處理函數:
以下為引用的内容: private void tmrWindowsFormsTimer_Tick(object sender, System.EventArgse) { ShowTimerEventFired(DateTime.Now, GetThreadName()); } private void tmrTimersTimer_Elapsed(object sender, System.TimersElapsedEventArgse){ ShowTimerEventFired(DateTime.Now, GetThreadName()); } private void tmrThreadingTimer_TimerCallback(object state){ ShowTimerEventFired(DateTime.Now, GetThreadName()); } |
正如你所看到的,ShowTimerEventFired方法采用目前時間和目前線程名字作為參數。為了差別工作者線程和UI線程,在例子程式的主入口點設定CurrentThread對象的名字屬性為"UIThread"。GetThreadName幫助函數傳回Thread.CurrentThread.Name值或者當Thread.CurrentThread.IsThreadPoolThread屬性為真時傳回"WorkerThread"。
因為System.Timers.Timer和System.Threading.Timer的定時器事件都是在工作者線程上執行的,是以在事件處理函數中的任何使用者互動代碼都不是馬上進行的,而是被列集等候傳回到UI線程上進行處理。為了這樣做,我建立了一個ShowTimerEventFiredDelegate委托調用:
以下為引用的内容: private delegate void ShowTimerEventFiredDelegate (DateTime eventTime, string threadName); ShowTimerEventFiredDelegate允許ShowTimerEventFired方法在UI線程上調用它自己,Figure 6顯示了發生這一切的代碼。 |
通過查詢InvokeRequired屬性可以非常容易的知道你是否從目前線程可以安全的通路Windows窗體控件。在這個例子中,如果清單框的InvokeRequired屬性為真,窗體的BeginInvoke方法就可以被ShowTimerEventFired方法調用,然後再被ShowTimerEventFiredDelegate方法調用。這能夠保證清單框的Add方法在UI線程上執行。
正如你所看到的,當你編寫異步定時器事件時有許多問題需要意識到。在使用System.Timers.Timer和System.Threading.Timer之前我推薦你閱讀Ian Griffith的文章“Windows Forms:Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads”, 該文刊登在MSDN雜志的2003年2月份的期刊上。
處理定時器事件重入
當和異步定時器事件打交道時,如由System.Timers.Timer和System.Threading.Timer産生的定時器事件,有另外一個細微之處你需要考慮。問題就是必須處理代碼重入。如果你的定時器事件處理函數代碼執行時間比你的定時器引發定時器事件的時間間隔要長,你預先又沒有采取必要的措施保護防止多線程通路你的對象和變量,你就會陷入調試的困境。看一下下面的代碼片斷:
以下為引用的内容: private int tickCounter = 0; private void tmrTimersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgse) { System.Threading.Interlocked.Increment(ref tickCounter); Thread.Sleep(5000); MessageBox.Show(tickCounter.ToString()); } |
假設你的定時器間隔屬性設定為1000毫秒,你也許會奇怪當第一個資訊框彈出時顯示的值是5。這是因為在這5秒期間第一個定時器事件正在睡眠,而定時器卻在不同的工作者線程上繼續産生時間消失事件。是以,在第一個定時器事件處理完成之前tickCounter變量被增加了5次。注意我使用了Interlocked.Increment方法以線程安全的方式增加tickCounter變量的值。也有其它方法可以這樣做,但是Interlock.Increment是為這種操作而特别設計的。
解決這種問題的簡單方法就是在你的事件處理函數代碼塊中暫時禁止定時器,接着再允許定時器,就像下面的代碼:
以下為引用的内容: private void tmrTimersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgse) { tmrTimers.Enabled = false; System.Threading.Interlocked.Increment(ref tickCounter); Thread.Sleep(5000); MessageBox.Show(tickCounter.ToString()); tmrTimersTimer.Enabled = true; } |
有了這段代碼,消息框就會每5秒鐘顯示一次,就像你所期望的那樣,tickCounter的值每次隻增加1。另外一些可選的原始同步對象就是Monitor或mutex去確定所有将來的事件被排隊直到目前的事件處理函數執行完成。
結論
為了快速友善的看到.NET架構中這三個定時器類的不同之處,見Figure 7對三個類的比較。當使用定時器類時有一點你要考慮的就是是否可以使用Windows排程器去定期的運作标準的可執行程式來更簡單的解決問題。
轉載于:https://www.cnblogs.com/netweb/archive/2008/10/30/1323315.html