熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

Net框架類庫中定時器類的使用

2013-11-13 09:44:17  來源: .NET編程 

  不論在客戶端應用程序還是服務器組件(包括窗口服務)定時器通常扮演一個重要的角色寫一個高效的定時器驅動型可管理代碼要求對程序流程有一個清晰的理解及掌握NET線程模型的精妙之處NET框架類庫提供了三種不同的定時器類SystemWindowsFormsTimer SystemTimersTimer 和SystemThreadingTimer每個類為不同的場合進行設計和優化本文章將研究這三個類並讓你理解如何及何時應該使用哪一個類

  Microsoft? Windows?裡的定時器對象當行為發生時允許你進行控制定時器一些最常用的地方就是有規律的定時啟動一個進程在事件之間設置間隔及當進行 圖形工作時維護固定的動畫速度(而不管處理函數的速度)在過去對於使用Visual Basic?的開發者來說定時器甚至用來模擬多任務

  正如你所期望的那樣對於你需要應對的不同場合微軟為你裝備了一些工具NET框架類庫中有三種不同的定時器類SystemWindowsFormsTimerSystemTimersTimer和SystemThreadingTimer頭兩個類出現在Visual Studio? NET的工具箱窗口這兩個定時器控件都允許你直接把它們拖拽到Windows窗體設計器或組件類設計器上如果你不小心這就是麻煩的開始

  Visual Studio NET工具箱上的Windows窗體頁和組件頁(見Figure )都有定時器控件非常容易的錯誤地使用它們當中的一個或者更糟糕的是根本意識不到它們的不同僅當目標是Windows窗體設計器時才使用Windows窗體頁上的定時器控件這個控件將在你的窗體上放置一個SystemsWindowsFormsTimer類的實例像工具箱上的其它控件一樣你可以讓Visual Studio NET處理其生成或者你自己手動的實例和初始化這個類

  Figure 定時器控件

  在組件頁上的定時器控件可以被安全的用在任何類中這個控件創建了一個SystemTimersTimer類的實例如果你正在使用Visual Studio NET工具箱無論是Windows窗體設計器還是組件類設計器你都可以安全的使用這個類在Visual Studio NET中當你設計一個派生於SystemComponentModelComponent的類時使用組件類設計器SystemThreadingTimer類不出現在Visual Studio NET工具箱窗口上它稍微有點復雜但提供了一個更高級別的控件稍後你會在本文章中看到

  Figure 例子程序

  讓我們首先研究SystemWindowsFormsTimer和SystemTimersTimer類這兩個類有著非常相似的對象模型稍後我將探索更加高級的SystemThreadingTimer類Figure 是我將在整個文章引用的例子程序的一個屏幕快照這個應用程序將會讓你獲得對這幾個定時器類的清晰的理解你可以從本文章的開始鏈接處下載完整的代碼並試驗它

  SystemWindowsFormsTimer

  如果你在找一個節拍器你已經走錯了地方了這個定時器類引發的定時器事件是同你的窗口應用程序的其余代碼相同步的這意味著正在執行的代碼從來不會被這個定時器類的實例所搶占(假設你不調用ApplicationDoEvents)就像一個典型窗體程序裡的其它代碼一樣任何駐留在一個定時器事件處理函數(指的是該類型的定時器類)中的代碼也是使用應用程序的UI線程所執行在空閒時候該UI線程同樣要對應用程序的窗體消息隊列中的所有消息進行負責這不僅包括由這個定時類引發的消息也包括窗體API消息無論何時你的程序不忙於做其它事情時該UI線程就處理這些消息

  在Visual Studio NET之前如果你寫過Visual Basic代碼你可能知道在一個窗口應用程序裡當正在執行一個事件處理函數時讓你的UI線程去響應其它窗體消息的唯一方法就是調用ApplicationDoEvents方法就像Visual Basic一樣NET框架中調用ApplicationDoEvents能夠產生許多問題ApplicationDoEvents產生了對UI消息泵的控制讓你對所有未處理的事件進行處理這能夠改變我剛才提到的所期望的執行路徑如果為了處理由該定時器類產生的定時器事件而在你的代碼中有一個ApplicationDoEvents的調用你的程序流程可能會被打斷這會產生不希望的行為並使調試困難

  運行例子程序就會使這個定時器類的行為變得清楚單擊程序的Start按鈕接著單擊Sleep按鈕最後單擊Stop按鈕將會產生下面的輸出結果

  SystemWindowsFormsTimer Started @ :: PM> Timer Event @ :: PM on ThreadUIThread> Timer EVENT @ :: PM on Thread: UIThread> Timer Event @ :: PM on Thread: UIThreadSleeping for ms> Timer Event @ :: PM on Thread: UIThreadSystemWindowsFormsTimer Stopped @ :: PM

  例子程序設置SystemWindowsFormsTimer類的間隔屬性為毫秒正如你所看到的當UI線程正在睡眠(秒)期間如果定時器事件處理函數仍然繼續捕捉定時器事件的話當睡眠線程再次被喚醒的時候應該有個定時器事件被顯示——在UI線程睡眠時每秒鐘一個然而當UI線程在睡眠時定時器卻保持掛起狀態

  對SystemWindowsFormsTimer的編程不能再簡單了——它有一個非常簡單和可直接編程的接口Start和Stop方法實際上提供了一個設置使能屬性的改變方法(其本身是對Win?的SetTimer和KillTimer功能的一個包裝)我剛才提到的間隔屬性名字本身就說明了問題即使技術上你可以設置間隔屬性低到毫秒但你應該知道在NET框架文檔中指出這個屬性大約精確到毫秒(假定UI線程對於處理是可用的)

  捕捉由SystemWindowsFormsTimer類實例引發的事件是通過感知一個標准的EventHandler委托的標記事件來處理的就像下面的代碼片斷所示

  SystemWindowsFormsTimer tmrWindowsFormsTimer = new SystemWindowsFormsTimer();tmrWindowsFormsTimerInterval = ;tmrWindowsFormsTimerTick += new EventHandler(tmrWindowsFormsTimer_Tick);tmrWindowsFormsTimerStart();private void tmrWindowsFormsTimer_Tick(object sender SystemEventArgs e){ //Do something on the UI thread}
  SystemTimersTimer

  NET框架文檔指出SystemTimersTimer類是一個服務器定時器是為多線程環境進行設計和優化該定時器類的實例能夠被多個線程安全地訪問不像SystemWindowsFormsTimerSystemTimersTimer缺省的將在一個工作者線程上調用你的定時器事件處理函數該工作者線程是從公共語言運行時(CLR)線程池中獲得這意味著在你的逝去的時間處理函數代碼中必須遵從Win編程的黃金規則除了創建該控件實例的線程之外一個控件的實例從來不被任何其它的線程所訪問

  SystemTimersTimer提供了一個簡單的方法處理這樣的困境——暴露一個公共的SynchronizingObject屬性把該屬性設置為一個窗體實例(或者窗體上的一個控件)將保證你的事件處理函數代碼運行在SynchronizingObject被實例化的同一個線程裡

  如果你使用了Visual Studio NET工具箱Visual Studio NET自動的設置SynchronizingObject屬性為當前的窗體實例首先它設定該定時器的SynchronizingObject屬性使其在功能上同SystemWindowsFormsTimer類一樣對於大部分功能的確是這樣當操作系統通知SystemTimersTimer類所允許的定時時間已過去定時器使用SynchronizingObjectBeginInvoke方法在一個線程上去執行事件委托該線程是創建SynchronizingObject的線程事件處理函數將被阻塞直到UI線程能夠處理它然而不像SystemWindowsFormsTimer類一樣該事件最終仍然能夠被引發像你在Figure 中看到的當UI線程不能夠處理時SystemWindowsFormsTimer不會引發事件可是當UI線程可用時SystemTimersTimer卻會排隊等候處理

  Figure 是如何使用SynchronizingObject屬性的例子使用例子程序並通過選擇SystemTimersTimer的radio按鈕你可以分析這個類並按照執行SystemWindowsFormsTimer類行為的同樣順序運行該類這樣就會產生Figure 的輸出結果

  正如你所看到的它不會跳過一個跳動——即使UI線程在睡眠在每一個事件間隔就有一個時間消失事件處理會被排隊執行因為UI線程在睡眠所以當UI線程一旦被喚醒例子程序就會列出個定時器事件()並能夠處理處理函數

  正如我早先提到的SystemTimersTimer類成員非常類似與SystemWindowsFormsTimer最大的區別就在與SystemTimersTimer類是對Win可等待定時對象的一個包裝並在工作者線程上產生一個時間片消失事件而不是在UI線程上產生一個時間標記事件時間片消失事件必須與一個同ElapsedEventHandler委托像匹配的事件處理函數相連接事件處理函數接受一個ElapsedEventArgs類型的參數

  除了標准的EventArgs成員ElapsedEventArgs類暴露了一個公共的SignalTime屬性它包含了一個精確的定時器時間片消失的時間因為這個類支持不同線程的訪問除了時間消失事件所在的線程應該相信它的Stop方法能夠被其它線程所調用這會潛在的導致消失事件被引發即使其Stop方法已經被調用你可以把SignalTime和Stop方法調用的時間進行比較來解決這個問題

  SystemTimersTimer也提供了AutoReset屬性來決定當時間片消失事件引發後是繼續進行還是只這一次要記住在定時器開始後重設間隔屬性會導致當前計數為

  比如設置了一個秒的間隔在間隔被改變為秒時秒已經過去了那麼下一個定時器事件將會在上一個定時器事件秒後發生

  SystemThreadingTimer

  第三個定時器類來自SystemThreading名字空間我願意說這是所有定時器類中最好的一個但這會引起誤導舉一個例子我驚訝的發現對於駐留在SystemThreading名字空間的這個類天生就不是線程安全的(很明顯這不意味著它不能以線程安全的方式使用)這個類的可編程接口同其它兩個類也不一致它稍微有點麻煩

  不像我開始描述的兩個定時器類SystemThreadingTimer有四個重載構造函數就像下面這樣

  public Timer(TimerCallback callback object state long dueTime long period);
public Timer(TimerCallback callback object state UInt dueTime UInt 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)讓你定義一個引發初始定時器事件的時間你可指定一個立即開始定時器或者阻止定時器自動的開始你可以使用SystemThreadingTimeoutInfinite常量

  第四個參數(period)讓你定義一個回調函數被調用的時間間隔(毫秒)給該參數定義一個或者TimeoutInfinite可以阻止後續的定時器事件調用

  一旦構造函數被調用你仍然可以通過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 automaticallySystemThreadingTimer tmrThreadingTimer = newSystemThreadingTimer(new TimerCallback(tmrThreadingTimer_TimerCallback) null SystemThreadingTimeoutInfinite );
//Manually start the timertmrThreadingTimerChange( );
//Manually stop the timertmrThreadingTimerChange(TimeoutInfinte TimeoutInfinite);

  正如你所期望的那樣通過選擇SystemThreadingTimer類運行例子程序會產生同你看到的SystemTimersTimer類一樣的輸出結果因為TimerCallback功能也是在工作者線程上被調用沒有一個跳動被跳過(假設有工作者線程可用)Figure 顯示了例子程序的輸出結果

  不像SystemTimersTimer類沒有與SynchronizingObject相對應的屬性被提供任何請求訪問UI控件的操作都必須通過控件的Invoke或BeginInvoke方法被列集

  定時器的線程安全編程

  為了最大限度的代碼重用三種不同類型的定時器事件都調用了同樣的ShowTimerEventFired方法下面就是三個定時器事件的處理函數

  private void tmrWindowsFormsTimer_Tick(object sender SystemEventArgse)
{
ShowTimerEventFired(DateTimeNow GetThreadName());
}
private void tmrTimersTimer_Elapsed(object sender SystemTimersElapsedEventArgse){
ShowTimerEventFired(DateTimeNow GetThreadName());
}
private void tmrThreadingTimer_TimerCallback(object state){ ShowTimerEventFired(DateTimeNow GetThreadName());
}

  正如你所看到的ShowTimerEventFired方法采用當前時間和當前線程名字作為參數為了區別工作者線程和UI線程在例子程序的主入口點設置CurrentThread對象的名字屬性為UIThreadGetThreadName幫助函數返回ThreadCurrentThreadName值或者當ThreadCurrentThreadIsThreadPoolThread屬性為真時返回WorkerThread

  因為SystemTimersTimer和SystemThreadingTimer的定時器事件都是在工作者線程上執行的所以在事件處理函數中的任何用戶交互代碼都不是馬上進行的而是被列集等候返回到UI線程上進行處理為了這樣做我創建了一個ShowTimerEventFiredDelegate委托調用

  private delegate void ShowTimerEventFiredDelegate (DateTime eventTime string threadName);

  ShowTimerEventFiredDelegate允許ShowTimerEventFired方法在UI線程上調用它自己

  Figure 顯示了發生這一切的代碼

  通過查詢InvokeRequired屬性可以非常容易的知道你是否從當前線程可以安全的訪問Windows窗體控件在這個例子中如果列表框的InvokeRequired屬性為真窗體的BeginInvoke方法就可以被ShowTimerEventFired方法調用然後再被ShowTimerEventFiredDelegate方法調用這能夠保證列表框的Add方法在UI線程上執行

  正如你所看到的當你編寫異步定時器事件時有許多問題需要意識到在使用SystemTimersTimer和SystemThreadingTimer之前我推薦你閱讀Ian Griffith的文章Windows Forms:Give Your NETbased Application a Fast and Responsive UI with Multiple Threads 該文刊登在MSDN雜志的月份的期刊上

  處理定時器事件重入

  當和異步定時器事件打交道時如由SystemTimersTimer和SystemThreadingTimer產生的定時器事件有另外一個細微之處你需要考慮問題就是必須處理代碼重入如果你的定時器事件處理函數代碼執行時間比你的定時器引發定時器事件的時間間隔要長你預先又沒有采取必要的措施保護防止多線程訪問你的對象和變量你就會陷入調試的困境看一下下面的代碼片斷

  private int tickCounter = ;
private void tmrTimersTimer_Elapsed(object sender SystemTimersElapsedEventArgse)
{
SystemThreadingInterlockedIncrement(ref tickCounter);
ThreadSleep();
MessageBoxShow(tickCounterToString());
}

  假設你的定時器間隔屬性設置為毫秒你也許會奇怪當第一個信息框彈出時顯示的值是這是因為在這秒期間第一個定時器事件正在睡眠而定時器卻在不同的工作者線程上繼續產生時間消失事件因此在第一個定時器事件處理完成之前tickCounter變量被增加了注意我使用了InterlockedIncrement方法以線程安全的方式增加tickCounter變量的值也有其它方法可以這樣做但是InterlockIncrement是為這種操作而特別設計的

  解決這種問題的簡單方法就是在你的事件處理函數代碼塊中暫時禁止定時器接著再允許定時器就像下面的代碼

  private void tmrTimersTimer_Elapsed(object sender SystemTimersElapsedEventArgse)
{
tmrTimersEnabled = false;
SystemThreadingInterlockedIncrement(ref tickCounter);
ThreadSleep();
MessageBoxShow(tickCounterToString());
tmrTimersTimerEnabled = true;
}

  有了這段代碼消息框就會每秒鐘顯示一次就像你所期望的那樣tickCounter的值每次只增加另外一些可選的原始同步對象就是Monitor或mutex去確保所有將來的事件被排隊直到當前的事件處理函數執行完成

  結論

  為了快速方便的看到NET框架中這三個定時器類的不同之處見Figure 對三個類的比較當使用定時器類時有一點你要考慮的就是是否可以使用Windows調度器去定期的運行標准的可執行程序來更簡單的解決問題


From:http://tw.wingwit.com/Article/program/net/201311/11449.html
    推薦文章
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.