http://www.dotblogs.com.tw/billchung/archive/2009/04/18/8044.aspx
Timer在.Net中也是個挺有趣的族群,在.Net Framework中有三種不一樣的Timer,分別是Windows.Forms.Timer、System.Timers.Timer、System.Threading.Timer。這三個時間人在某些地方有點相同,也有許多地方大異其趣,所以我一直覺得他們是很有意思的。
先來看看MSDN是如何說明這三個Timer,在MSDN的[伺服器端計時器簡介] 有一段話是這麼說的:
<以下節錄自MSDN>
伺服器計時器、Windows 計時器和執行緒計時器
Visual Studio 和 .NET Framework 中有三個計時器控制項,也就是可於 [工具箱] 的 [元件] 索引標籤上看到的伺服器端計時器、在 [工具箱] 的 [Windows Form] 索引標籤上看到的標準 Windows 架構計時器,以及只能以程式方式使用的執行緒計時器。Windows 架構計時器從 Visual Basic 版本 1.0 就有了,且一直維持著並無本質上的改變。這個計時器最適合在 Windows Form 應用程式中使用。伺服器端的計時器是傳統計時器的更新,它在伺服器的環境中最適合使用。執行緒計時器是簡單的輕量計時器,使用回呼方法而不使用事件,同時由執行緒集區執行緒提供服務。
在 Win32 的架構下有兩種執行緒:UI 執行緒,以及背景工作執行緒。UI 執行緒在大多數的時間裡都保持著閒置的狀態並且等待訊息到達它的訊息迴圈中。一旦收到訊息,它便處理這個訊息然後等待下一個訊息的到達。另外,背景工作執行緒則是用於執行背景處理而不使用訊息迴圈。Windows 計時器和伺服器端計時器兩者都使用 Interval 屬性執行。執行緒計時器的間隔是在 Timer 建構函式中設定。計時器是針對不同目的而設計的,依執行緒的處理方式做為辨識:
-
Windows 計時器是設計用在單一執行緒的環境之下,在此,利用 UI 執行緒來執行處理。Windows 計時器的正確率限制在 55 毫秒。這些傳統的計時器需要使用者的程式碼中有一個可以使用的 UI 訊息幫浦,並且一直從相同的執行緒作業,或是將呼叫整理到另外一個執行緒。對於一個 COM 元件,這可能對效能有不利的影響。
-
伺服器端計時器是為了在多執行緒環境下使用背景工作執行緒而設計的。因為它們使用不同的架構,伺服器端計時器可能要比 Windows 計時器更為精確。伺服器端計時器可以在執行緒間移動以處理被引發的事件。
-
執行緒計時器對於執行緒上不提取訊息的案例中相當有用。例如,Windows 架構計時器仰賴作業系統的計時器支援,而如果您沒有提取執行緒上的訊息,與計時器關聯的事件就不會發生。執行緒計時器在這種情況中更有用。
老實說,第一次看的時候還真是有看沒有懂,這篇文章大部份的字我都認識,但連起來之後就完全搞糊塗了,那就只好自己動手做做看,體會一下這些內容是在描述些什麼。
首先來瞧瞧最傳統的Windows.Forms.Timer,這種計時器應該是最簡便使用的,簡便到可以直接在設計表單畫面時直接從工具箱拉一個元件來用,這個計時器的時間間隔是以Interval 屬性來設定時間﹝以千分之一秒為單位﹞,用過Windows.Forms.Timer的人應該都知道,不過特別強調的一點是在Windows.Forms.Timer的Interval 屬性是存在一些限制的,詳細可以參考[MSDN:Windows Form Timer 元件的 Interval 屬性限制] 。簡要而言有幾個重點:
(1)你只能設定 1~64,767毫秒,也就是一個間隔最大只有一分鐘出頭。 『2009/04/19註記:這個限制現在似乎不成立,我後來發現新版的MSDN文件已經取消了這一項說明[MSDN:(.NET 3.5) Windows Form Timer 元件的 Interval 屬性限制];奇怪的是,我是用VB2005(.NET 2.0 SP1),卻也沒有這項限制,所以有可能一開始是微軟沒有修正到這個說明的內容。』
(2)它的時間比另外兩個計時器不準。
(3)如果在Tick事件中要處理的程序時間太長,它可能會出包。
既然如此,那這個計時器還有存在的必要嗎?我覺得它最大的優點是容易使用,而且不需要使用委派就可以直接呼叫表單畫面的控制項─這對於已經慣於寫多執緒的網友們當然不算什麼困擾,但對於不習於撰寫多執行緒程式,或是程式只要處理簡單的循環程序,Windows.Forms.Timer不失為一個方便的選擇。
現在來看看Windows.Forms.Timer的示範,在以下畫面中只有幾個簡單的控制項,其中NumericUpDown1控制項可以調整nterval 屬性值,另外寫了兩個用來測試Tick Event的方法,程式內容沒什麼新奇的:
Public Class Form1
Dim FormTimer As New Windows.Forms.Timer
Dim iTickTimes As Integer = 1
Private Sub BTN_FrmT_start_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BTN_FrmT_start.Click
FormTimer.Interval = NumericUpDown1.Value
'可以修改使用不同的Handler Method,觀察TextBox的內容是否有所不同
' AddHandler FormTimer.Tick, AddressOf myTimerTick01 'Form timer 用Tick事件程序1
AddHandler FormTimer.Tick, AddressOf myTimerTick02 'Form timer 用Tick事件程序2
FormTimer.Start()
End Sub
Private Sub myTimerTick01(ByVal sender As System.Object, ByVal e As System.EventArgs)
TB_F.Text &= "#" & iTickTimes & " Tick Begin:" & Environment.TickCount & vbCrLf
Dim i As Integer
For i = 0 To 1
System.Threading.Thread.Sleep(1)
Next
Application.DoEvents()
TB_F.Text &= "#" & iTickTimes & " Tick End:" & Environment.TickCount & vbCrLf
iTickTimes += 1
End Sub
Private Sub myTimerTick02(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim imyTicks As Integer
imyTicks = iTickTimes
iTickTimes += 1
TB_F.Text &= "#" & imyTicks & " Tick Begin:" & Environment.TickCount & vbCrLf
Dim i As Integer
For i = 0 To 1
System.Threading.Thread.Sleep(1)
Next
Application.DoEvents()
TB_F.Text &= "#" & imyTicks & " Tick End:" & Environment.TickCount & vbCrLf
End Sub
Private Sub BTN_FrmT_stop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BTN_FrmT_stop.Click
FormTimer.Stop()
TB_F.Text &= "Stop:" & Environment.TickCount & vbCrLf
End Sub
Private Sub BTN_Clear_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BTN_Clear.Click
TB_F.Clear()
iTickTimes = 1
End Sub
End Class
首先,我們將Interval設定為1,使用myTimerTick02方法來個瘋狂跑馬:
所有的End訊號居然都發生在Timer.Stop之後,而且順序上看起來也很奇特,這表示說,如果我們不讓Timer Stop,在沒有足夠時間完成Tick事件所呼叫的方法的狀態下,它是永遠無法完成該完成的工作,證實了[如果在Tick事件中要處理的程序時間太長,它可能會出包] ;至於時間間隔不正確的問題,可以看到兩個Begin之間的差距在30毫秒上下,這可能是因為我的電腦根本沒法在這麼短的時間切換到下一個Tick,所以這個數據可能有點不客觀,不過等會將間隔放大後,就可以看出個大概了。
這一次咱們將Interval設為100,同樣使用myTimerTick02方法來個瘋狂跑馬:
看起來這次比較乖巧了,有按照順序一個循環接一個循環的完成工作。這邊來驗證時間的問題,在右方的結果中,#2的Tcik Begin 減去 #1的Tick Begin是109;#4減去#3則是110,看起來就是有差一點點,果然時間並不是很準,不過差距很小也沒啥好挑替的。
這程式中還附了另一個myTimerTick01,各位可以試著使用它來當做Tick呼叫的Handler Method,看看和myTimerTick02會出現哪些不同的結果。下一篇會再來聊聊System.Timers.Timer吧!
這個範例程式是以VB2005寫的,可以在以下超連結下載FormTimerTest.rar。
http://www.dotblogs.com.tw/billchung/archive/2009/04/19/8052.aspx
第二個要談到的時間人就是System.Timers.Timer了,System.Timers.Timer和Windows.Forms.Timer有一些個不同點,簡單列幾個比較重要的如下:
(1)System.Timers.Timer的Interval屬性值的資料型別是Double;而在Windows.Forms.Timer中這個屬性植是Integer。
(2)System.Timers.Timer中引發的事件名稱為Elapsed;Windows.Forms.Timer則使用Tick事件。
(3)System.Timers.Timer使用背景執行緒來處理Elapsed事件所要處理的內容;Windows.Forms.Timer則是在UI執行緒,也就是表單畫面執行緒中處理Tick事件。
(4)當System.Timers.Timer的Elapsed事件程序內容要變更表單畫面控制項時,需使用委派的方式或透過SynchronizingObject 屬性封送來處理。
(5)System.Timers.Timer可藉由AutoReset屬性的設定決定是否要不斷的循環。
以下我們就拿個實際的例子來測試System.Timers.Timer的功能,畫面和三種時間人《.NET中的Timer(1)》差不多,就是多了個AutoRest的選項,先來看一下程式的內容:
Public Class Form1
Dim SystemTimer As New System.Timers.Timer
Dim iTickTimes As Integer = 1
Delegate Sub SetMsg1Callback(ByVal InputString As String)
Private Sub DisplayMsg1(ByVal strReceive As String)
Try
If Me.TB_S.InvokeRequired Then
Dim d As New SetMsg1Callback(AddressOf DisplayMsg1)
Me.Invoke(d, New Object() {strReceive})
Else
Me.TB_S.Text &= strReceive
End If
Catch ex As ObjectDisposedException
'停止的時候有可能會造成 ObjectDisposedException
'請參閱 [Try Catch能幫你做什麼(4)?] http://www.dotblogs.com.tw/billchung/archive/2009/04/04/7851.aspx
End Try
End Sub
Private Sub Btn_SysT_start_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Btn_SysT_start.Click
SystemTimer.Interval = NumericUpDown1.Value
SystemTimer.AutoReset = CheckBox1.Checked
'可以修改使用不同的Handler Method,觀察TextBox的內容是否有所不同
' AddHandler SystemTimer.Elapsed, AddressOf myTimerElapsed01 'Form timer Tick事件程序1
AddHandler SystemTimer.Elapsed, AddressOf myTimerElapsed02 'Form timer 用Tick事件程序2
TB_S.Text &= "Start:" & Environment.TickCount & vbCrLf
SystemTimer.Start()
End Sub
Private Sub BTN_SysT_stop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BTN_SysT_stop.Click
SystemTimer.Stop()
'RemoveHandler SystemTimer.Elapsed, AddressOf myTimerElapsed01 'Form timer Tick事件程序1
RemoveHandler SystemTimer.Elapsed, AddressOf myTimerElapsed02 'Form timer 用Tick事件程序2
TB_S.Text &= "Stop:" & Environment.TickCount & vbCrLf
End Sub
Private Sub BTN_Clear_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BTN_Clear.Click
TB_S.Clear()
iTickTimes = 1
End Sub
Private Sub myTimerElapsed01(ByVal sender As System.Object, ByVal e As System.Timers.ElapsedEventArgs)
DisplayMsg1("#" & iTickTimes & " Tick Begin:" & Environment.TickCount & vbCrLf)
Looping()
DisplayMsg1("#" & iTickTimes & " Tick End:" & Environment.TickCount & vbCrLf)
iTickTimes += 1
End Sub
Private Sub myTimerElapsed02(ByVal sender As System.Object, ByVal e As System.Timers.ElapsedEventArgs)
Dim imyTicks As Integer
imyTicks = iTickTimes
iTickTimes += 1
DisplayMsg1("#" & imyTicks & " Tick Begin:" & Environment.TickCount & vbCrLf)
Looping()
DisplayMsg1("#" & imyTicks & " Tick End:" & Environment.TickCount & vbCrLf)
End Sub
Private Sub Looping()
Dim i As Integer
'可以修改迴圈數,觀察程式的變化
For i = 0 To 1
System.Threading.Thread.Sleep(1)
Next
End Sub
End Class
來看看執行的結果 ,可以發現它在Elapsed事件中要處理的程序時間超過Interval屬性值也不怎麼乖巧,不過和Windows.Forms.Timer有一個不同點是,它會把事情做完,但同一件事可能會做好多次,可以從圖上看的出來有些程序還沒End,下一個程序就Begin了。而且完成的順序也會出現混亂的狀況。
接下來一樣把間隔時間改為100,果然執行結果就正常的許多,順便觀察一下時間的差異,準確度其實和之前的Windows.Forms.Timer差不多,也許要做很大量的數據才能比較出差異吧。
來看一下AutoReset,當我們將AutoReset屬性設為False時,可以發現Elapsed事件僅僅被執行了一次,在這種狀況下,若要再次執行必須再呼叫一次SystemTimer.Start() 方法。這個範例程式是以VB2005撰寫的,可以在以下超連結下載SysTimerTest.rar。
最後談一下如何使用SynchronizingObject 屬性,這個屬性會使得System.Timers.Timer處理Elapsed事件所要處理的內容之時產生與Windows.Forms.Timer處理Tick事件類似的結果,也就是當System.Timers.Timer執行個體設定了SynchronizingObject 屬性之後,它會和此屬性所指向的物件在相同的執行緒上引發Elapsed事件。以下是一個簡單的設定例子:
Private Sub Btn_SysT_start_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Btn_SysT_start.Click
SystemTimer.Interval = NumericUpDown1.Value
SystemTimer.AutoReset = CheckBox1.Checked
SystemTimer.SynchronizingObject = Me
'可以修改使用不同的Handler Method,觀察TextBox的內容是否有所不同
' AddHandler SystemTimer.Elapsed, AddressOf myTimerElapsed01 'Form timer Tick事件程序1
AddHandler SystemTimer.Elapsed, AddressOf myTimerElapsed03 'Form timer 用Tick事件程序2
TB_S.Text &= "Start:" & Environment.TickCount & vbCrLf
SystemTimer.Start()
End Sub
Private Sub myTimerElapsed03(ByVal sender As System.Object, ByVal e As System.Timers.ElapsedEventArgs)
' 使用 SynchronizingObject 這邊就沒使用委派了
TB_S.Text &= "#" & iTickTimes & " Tick Begin:" & Environment.TickCount & vbCrLf
Looping()
TB_S.Text &= "#" & iTickTimes & " Tick End:" & Environment.TickCount & vbCrLf
iTickTimes += 1
End Sub
http://www.dotblogs.com.tw/billchung/archive/2009/05/03/8277.aspx
第三種Timer就是System.Threading.Timer﹝以下簡稱為Threading.Timer﹞,和前兩個Timer不同的是,Threading.Timer是使用回呼﹝Callback﹞方式而非使用事件﹝Event﹞來執行其工作,在MSDN文件庫中的System.Threading命名空間中的[Timer 成員]可以看到這個類別是不具備事件的。在System.Threading命名空間中的[Timer 建構函式]可以看的出來這種Timer使用[TimerCallback 委派]來委派一個方法給予Threading.Timer來呼叫,如果要使用包含State Object的建構函式,可以參考[回呼的秘密花園]一文中關於State Object的介紹。所以在這篇文章中範例程式的開頭就是先建立一個Threading.Timer的執行個體,並且委派一個方法給這個執行個體:
Dim ThreadTimer As New System.Threading.Timer(AddressOf ThreadTimerProc02)
Threading.Timer和System.Timers.Timer一樣屬於多執行緒形態的時間人,但是System.Timers.Timer可以使用SynchronizingObject屬性來使得我們可以不需要以委派的方法在程序中處理畫面中的控制項,因此當Threading.Timer所執行的程序要呼叫到畫面控制項時,就一定得用委派的方法處理不可;事實上,Threading.Timer類別只具備了建構函式與方法,表面上看起來的構造極為簡單,我想這就是為何在MSDN文件庫中會稱其為「簡單的輕量計時器」﹝詳見[三種時間人《.NET中的Timer(1)]中的伺服器計時器、Windows 計時器和執行緒計時器﹞。
System.Timers.Timer與Windows.Forms.Timer使用Interval屬性來設定時間的間隔,而Threading.Timer的時間間隔則是在[Timer.Change 方法]中以傳入參數的方式表示。Threading.Timer.Change包含了四個多載,但不論哪一個多載,其第一個參數代表的是「變更開始的時間﹝dueTime﹞」;第二個參數則是代表「計時器的方法引動過程之間的時間間隔﹝period﹞」。
1.dueTime:指的是當執行Timer.Change方法後延遲多久時間將會執行第一次的呼叫﹝以上面的範例就是呼叫ThreadTimerProc02方法﹞。當這個值為Infinite常數時﹝這個值其實就等於 -1﹞,意謂著無限期延遲等待,也就是將永遠不會執行呼叫。
2.period:指的是在Timer.Chang中的程序已被執行過一次之後,間隔多久時間再執行下一次所呼叫的方法。當這個值為Infinite常數時﹝這個值其實就等於 -1﹞或是0時,意謂著不會有下一次的呼叫﹝有點類似在System.Timers.Timer中將AutoReset屬性設定為False一般﹞。
關於整個範例程式的操作其實和先前的相關文章差不多,我想就不要浪費篇幅介紹,有興趣的網友可以自行下載範例程式玩一玩。範例程式是以VB2005撰寫的,請點選以下超連結下載:ThreadTimerTest.rar