摘要:.NET 框架提供了新的類,可以方便地創建多線程應用程序。本文介紹如何使用 Visual Basic® .NET 的多線程編程技術來開發效率更高、響應速度更快的應用程序。
目錄
- 簡介
- 多線程處理的優點
- 創建新線程
- 同步線程
- 線程計時器
- 取消任務
- 總結
簡介
過去,Visual Basic 開發人員創建的應用程序都是程序任務依次執行的同步應用程序。雖然多線程應用程序因多個任務幾乎同時運行而具有更高的效率,但使用早期版本的 Visual Basic 來創建這樣的應用程序卻很困難。
一項稱為多任務處理的操作系統功能使多線程程序成為可能,它能模擬同時運行多個應用程序的功能。雖然多數個人計算機都只安裝了一個處理器,但現代操作系統通過將處理器時間分配給多段可執行代碼(稱為線程),提供了多任務處理功能。線程可以代表整個應用程序,但通常只代表應用程序中可單獨運行的一部分。操作系統根據線程的優先級、上次運行線程后經過的時間等因素為每個線程分配處理時間。在執行耗時的任務(如文件輸入和輸出)時,多線程能夠顯著提高性能。
但要注意一個問題。雖然多線程可以提高性能,但每個線程都需要額外的內存來創建線程,還需要處理器時間來運行線程。如果創建的線程過多,反而會降低應用程序的性能。在設計多線程應用程序時,應在添加更多線程所獲得的好處及其成本之間進行權衡。
多任務處理成為操作系統的一部分已經很長時間了。但直到最近,Visual Basic 程序員也只能通過非正式發布的功能,來執行多線程任務,或者通過使用 COM 組件或操作系統的異步組件,來間接實現此功能。而 .NET 框架在 System.Threading 命名空間中為開發多線程應用程序提供了全面的支持。
本文討論多線程的一些優點以及如何使用 Visual Basic .NET 來開發多線程應用程序。雖然 Visual Basic .NET 和 .NET 框架使多線程應用程序的開發變得很簡單,但本文主要面向中高級開發人員,以及正在從 Visual Basic 的早期版本過渡到 Visual Basic .NET 的開發人員。對於 Visual Basic .NET 的初學者,請首先閱讀 Visual Basic Language Tour(英文)中的相應主題。
本文並非是對多線程編程的全面討論。要獲得更多的信息,請參閱本文最后列出的其他資源。
多線程處理的優點
同步應用程序的開發比較容易,但由於需要在上一個任務完成后才能開始新的任務,所以其效率通常比多線程應用程序低。如果完成同步任務所用的時間比預計時間長,應用程序可能會不響應。多線程處理可以同時運行多個過程。例如,文字處理器應用程序在您處理文檔的同時,可以檢查拼寫(作為單獨的任務)。由於多線程應用程序將程序划分成獨立的任務,因此可以在以下方面顯著提高性能:
- 多線程技術使程序的響應速度更快,因為用戶界面可以在進行其他工作的同時一直處於活動狀態。
- 當前沒有進行處理的任務可以將處理器時間讓給其他任務。
- 占用大量處理時間的任務可以定期將處理器時間讓給其他任務。
- 可以隨時停止任務。
- 可以分別設置各個任務的優先級以優化性能。
是否需要創建多線程應用程序取決於多個因素。在以下情況下,最適合采用多線程處理:
- 耗時或大量占用處理器的任務阻塞用戶界面操作。
- 各個任務必須等待外部資源(如遠程文件或 Internet 連接)。
例如,用於跟蹤 Web 頁上的鏈接並下載滿足特定條件的文件的 Internet 應用程序“robot”。這種應用程序可以依次同步下載各個文件,也可以使用多線程同時下載多個文件。多線程方法比同步方法的效率高很多,因為即使在某些線程中遠程 Web 服務器的響應非常慢,也可以下載文件。
創建新線程
創建線程最直接的方法是創建新的線程類實例,並使用 AddressOf 語句為要運行的過程傳遞委托。例如,以下代碼將名為 SomeTask
的子過程作為單獨的線程運行。
Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask) Thread1.Start ' 此處的代碼立即運行。
以上所述就是創建和啟動線程的方法。在線程 Start 方法調用之后的任何代碼將立即運行,而無需等待前一個線程運行結束。
下表列出了用於控制各個線程的一些方法。
方法 | 操作 |
---|---|
Start | 使線程開始運行。 |
Sleep | 使線程暫停一段指定的時間。 |
Suspend | 使線程在到達安全點后暫停。 |
Abort | 使線程在到達安全點后停止。 |
Resume | 重新啟動掛起的線程。 |
Join | 使當前線程等待其他線程運行結束。如果使用超時值,且線程在分配的時間內結束,此方法將返回 True。 |
多數方法都無需再加以說明,但“安全點”可能是個新的概念。安全點是指代碼中的某些位置,在這些位置公共語言運行時可以安全地執行自動垃圾回收,即釋放未使用的變量並回收內存。調用線程的 Abort 或 Suspend 方法時,公共語言運行時將分析代碼並確定線程停止運行的適當位置。
線程還包含許多有用的屬性,如下表所示:
屬性 | 值 |
---|---|
IsAlive | 如果線程處於活動狀態,則包含值 True。 |
IsBackground | 獲取或設置布爾值,指示線程是否是后台線程或是否應該是后台線程。后台線程與前台線程類似,但后台線程並不阻止進程的終止。當進程的所有前台線程都終止后,公共語言運行時將對仍處於活動狀態的后台線程調用 Abort 方法,以結束該進程。 |
Name | 獲取或設置線程的名稱。常用於在調試時查找各個線程。 |
Priority | 獲取或設置操作系統用來確定線程優先級安排的值。 |
ApartmentState | 獲取或設置用於特定線程的線程模型。當線程調用非托管的代碼時,線程模型將非常重要。 |
ThreadState | 包含說明線程狀態的值。 |
線程屬性和方法對創建和管理線程非常有用。本文的線程同步部分將介紹如何使用這些屬性和方法控制和協調線程。
線程參數和返回值
前面示例中的方法調用不能包含任何參數或返回值。這一限制是使用此方法創建和運行線程的主要缺點之一。然而,可以通過將在單獨的線程中運行的過程包裝到類或結構中,為它們提供參數,並使之能返回參數。
Class TasksClass Friend StrArg As String Friend RetVal As Boolean Sub SomeTask() ' 將 StrArg 字段用作參數。 MsgBox("StrArg 包含字符串" & StrArg) RetVal = True ' 設置返回參數的返回值。 End Sub End Class ' 要使用類,請設置存儲參數的屬性或字段, ' 然后,根據需要異步調用方法。 Sub DoWork() Dim Tasks As New TasksClass() Dim Thread1 As New System.Threading.Thread( _ AddressOf Tasks.SomeTask) Tasks.StrArg = "某個參數" ' 設置用作參數的字段。 Thread1.Start() ' 啟動新線程。 Thread1.Join() ' 等待線程 1 運行結束。 ' 顯示返回值。 MsgBox("線程 1 返回值" & Tasks.RetVal) End Sub
手動創建和管理線程最適合需要控制細節(例如線程優先級和線程模型)的應用程序。可以想象,使用這種方法管理大量線程將是非常困難的。如果需要很多線程,可以考慮使用線程池以降低復雜程度。
線程池
線程池是多線程的一種形式。在線程池中,當創建線程時任務被添加到隊列並自動啟動。使用線程池,可以使用要運行的過程的委托來調用 Threadpool.QueueUserWorkItem 方法,Visual Basic .NET 將創建線程並運行該過程。以下示例說明了如何使用線程池啟動多個任務。
Sub DoWork() Dim TPool As System.Threading.ThreadPool ' 將一個任務排隊 TPool.QueueUserWorkItem
(New System.Threading.WaitCallback _ (AddressOf SomeLongTask)) ' 將另一個任務排隊 TPool.QueueUserWorkItem
(New System.Threading.WaitCallback _ (AddressOf AnotherLongTask)) End Sub
如果要啟動很多單獨的任務,但並不需要單獨設置每個線程的屬性,則線程池將非常有用。每個線程都以默認的堆棧大小和優先級啟動。默認情況下,每個系統處理器上最多可以運行 25 個線程池線程。超過該限制的其他線程會被排隊,直至其他線程運行結束后它們才能開始運行。
線程池的一個優點是可以將狀態對象中的參數傳遞到任務過程。如果正在調用的過程需要多個參數,則可以將類的結構或實例強制轉換為 Object 數據類型。
參數和返回值
從線程池線程返回值有點復雜。不允許使用從函數調用返回值的標准方法,因為只有 Sub 過程可以排隊進入線程池。提供參數和返回值的一種方法是將參數、返回值和方法包裝到包裝類中,如線程參數和返回值中所述。一種更簡單的提供參數和返回值的方法,是使用 QueueUserWorkItem 方法的 ByVal 狀態對象變量(可選)。如果使用此變量將引用傳遞給類的實例,則該實例的成員便可以由線程池線程修改並用作返回值。您可以修改由變量(通過值傳遞)引用的對象,這在開始可能並非顯而易見,但的確是可能的,因為只有對象引用是通過值傳遞的。對由對象引用所引用的對象成員進行更改之后,這些更改將應用於實際的類實例。
不能使用結構返回狀態對象中的值。因為結構是值類型,異步進程所作的更改並不更改原始結構的成員。如果不需要返回值,則可以使用結構提供參數。
Friend Class StateObj Friend StrArg As String Friend IntArg As Integer Friend RetVal As String End Class Sub ThreadPoolTest() Dim TPool As System.Threading.ThreadPool Dim StObj1 As New StateObj() Dim StObj2 As New StateObj() ' 設置一些字段,用作狀態對象中的參數。 StObj1.IntArg = 10 StObj1.StrArg = "某個字符串" StObj2.IntArg = 100 StObj2.StrArg = "另一個字符串" ' 將一個任務排隊 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _ (AddressOf SomeOtherTask), StObj1) ' 將另一個任務排隊 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _ (AddressOf AnotherTask), StObj2) End Sub Sub SomeOtherTask(ByVal StateObj As Object) ' 將狀態對象字段用作參數。 Dim StObj As StateObj StObj = CType(StateObj, StateObj) ' 強制轉換為正確的類型。 MsgBox("StrArg 包含字符串" & StObj.StrArg) MsgBox("IntArg 包含數字" & CStr(StObj.IntArg)) ' 將字段用作返回值。 StObj.RetVal = "SomeOtherTask 的返回值" End Sub Sub AnotherTask(ByVal StateObj As Object) ' 將狀態對象字段用作參數。 ' 狀態對象作為 Object 進行傳遞。 ' 將其強制轉換為特定的類型以使其更易於使用。 Dim StObj As StateObj StObj = CType(StateObj, StateObj) MsgBox("StrArg 包含字符串 " & StObj.StrArg) MsgBox("IntArg 包含數字" & CStr(StObj.IntArg)) ' 將字段用作返回值。 StObj.RetVal = "AnotherTask 的返回值" End Sub
公共語言運行時自動為排隊的線程池任務創建線程,然后,當任務完成后釋放這些資源。將任務排隊后,很難再將其取消。ThreadPool 線程始終使用多線程單元 (MTA) 線程模型來運行。如果需要使用單線程單元 (STA) 模型的線程,則應手動創建線程。
同步線程
同步在多線程編程的非結構化性質與同步處理的結構化次序之間提供了一個折衷的辦法。
使用同步技術,可以完成以下操作:
- 在必須以特定順序執行任務時,顯式控制代碼運行的次序。
- 或者 -
- 當兩個線程同時共享相同的資源時,避免可能出現的問題。
例如,可以使用同步使顯示過程處於等待狀態,直至在另一線程中運行的數據檢索過程結束。
同步的方法有兩種:輪詢和使用同步對象。輪詢反復從循環中檢查異步調用的狀態。使用輪詢管理線程的效率最低,因為反復檢查各種線程屬性的狀態會浪費大量資源。
例如,如果輪詢要查看線程是否已結束,可以使用 IsAlive 屬性。使用此屬性時要很小心,因為活動的線程不一定正在運行。可以使用線程的 ThreadState 屬性來獲得有關線程狀態的詳細信息。由於在任意給定時間,線程都可能處於多種狀態,因此 ThreadState 中存儲的值可以是 System.Threading.Threadstate 枚舉中的值的組合。因此,在輪詢時應當仔細檢查所有相關的線程狀態。例如,如果線程的狀態表明它沒有運行,則該線程可能已經完成。另一方面,它也可能被掛起或處於休眠狀態。
可以想象,輪詢為控制運行線程的次序,犧牲了多線程的部分優點。為此,可以使用效率較高的 Join 方法來控制線程。Join 使調用過程處於等待狀態,直至線程完成或調用超時(如果指定了超時)。“Join”這個名稱來自這一想法,即創建的新線程是執行路徑的一個分支。使用 Join 可以再次將單獨的執行路徑合並成一個線程。
圖 1:線程
有一點需要清楚:Join 是同步調用或阻塞調用。調用 Join 或等待句柄的等待方法后,調用過程將停止並等待線程發出信號通知它已經完成。
Sub JoinThreads() Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask) Thread1.Start() Thread1.Join() ' 等待線程運行結束。 MsgBox("線程運行結束") End Sub
這些控制線程的簡單方法在管理少量線程時非常有用,但不適合大型項目。下一節將討論可用於同步線程的一些高級技術。
高級同步技術
多線程應用程序通常使用等待句柄和監視器對象來同步多個線程。下表介紹了可用於同步線程的部分 .NET 框架類。
類 | 用途 |
---|---|
AutoResetEvent | 等待句柄,用於通知一個或多個等待線程發生了一個事件。AutoResetEvent 在等待線程被釋放后自動將狀態更改為已發出信號。 |
Interlocked | 為多個線程共享的變量提供原子操作。 |
ManualResetEvent | 等待句柄,用於通知一個或多個等待線程發生了一個事件。手動重置事件的狀態將保持為已發出信號,直至 Reset 方法將其設置為未發出信號狀態。同樣,該狀態將保持為未發出信號,直至 Set 方法將其設置為已發出信號狀態。當對象的狀態為已發出信號時,任意數量的等待線程(即通過調用一個等待函數開始對指定事件對象執行等待操作的線程)都可以被釋放。 |
Monitor | 提供同步訪問對象的機制。Visual Basic .NET 應用程序調用 SyncLock 以使用監視器對象。 |
Mutex | 等待句柄,可用於進程間同步。 |
ReaderWriterLock | 定義用於實現單個寫入者和多個讀取者的鎖定。 |
Timer | 提供按指定間隔運行任務的機制。 |
WaitHandle | 封裝操作系統特有的、等待對共享資源進行獨占訪問的對象。 |
等待句柄
等待句柄是將一個線程的狀態通知另一個線程的對象。線程可以使用等待句柄,通知其他線程它們需要對資源進行獨占訪問。然后,其他線程必須等到沒有線程在使用等待句柄時才能使用此資源。等待句柄有兩種狀態:已發出信號和未發出信號。不屬於任何線程的等待句柄處於已發出信號狀態。屬於某線程的等待句柄處於未發出信號狀態。
線程通過調用一種等待方法(例如 WaitOne、WaitAny 或 WaitAll)來請求等待句柄的所有權。等待方法是與單獨線程的 Join 方法相類似的阻塞調用。
- 如果沒有其他線程擁有該等待句柄,則調用將立即返回 True,等待句柄的狀態將更改為未發出信號,而擁有等待句柄的線程將繼續運行。
- 如果線程調用了等待句柄的一種等待方法,但該等待句柄歸另一線程所有,則調用線程將等待指定的時間(如果指定了超時),或者無限期地等待(未指定超時),直至其他線程釋放等待句柄。如果指定了超時,並且在超時到期前釋放等待句柄,則調用返回 True。否則,調用返回 False,並且進行調用的線程將繼續運行。
擁有等待句柄的線程在運行結束后,或不再需要等待句柄時將調用 Set 方法。其他線程通過調用 Reset 方法,或者調用 WaitOne、WaitAll 或 WaitAny 以及成功地等待某一線程調用 Set 方法之后,可以將等待句柄的狀態重置為未發出信號。在單個等待線程被釋放后,系統將 AutoResetEvent 句柄自動重置為未發出信號。如果沒有線程處於等待狀態,則事件對象的狀態將保持為已發出信號。
方法 | 用途 |
---|---|
WaitOne | 接受一個等待句柄作為參數,並使調用線程處於等待狀態,直至另一個進程調用 Set 將當前的等待句柄設置為已發出信號。 |
WaitAny | 接受一個等待句柄數組作為參數,並使調用線程處於等待狀態,直至任一指定的等待句柄已通過調用 Set 設置為已發出信號。 |
WaitAll | 接受一個等待句柄數組作為參數,並使調用線程處於等待狀態,直至所有指定的等待句柄已通過調用 Set 設置為已發出信號。 |
Set | 將指定的等待句柄的狀態設置為已發出信號,並使任何等待線程繼續運行。 |
Reset | 將指定事件的狀態設置為未發出信號。 |
Visual Basic .NET 常用的等待句柄有三種:互斥對象、ManualResetEvent 和 AutoResetEvent。后兩種通常稱為同步事件。
互斥對象
互斥對象是一次只能由一個線程擁有的同步對象。實際上,“互斥”這個名稱來自互斥對象的所有權相互排斥這一事實。如果線程要對資源進行獨占訪問,則需要請求互斥對象的所有權。由於在任何時刻,只能有一個線程擁有互斥對象,因此其他線程必須等待,直至獲得互斥對象的所有權后才能使用資源。
WaitOne 方法使調用線程等待獲得互斥對象的所有權。如果擁有互斥對象的線程正常終止,則互斥對象的狀態將設置為已發出信號,下一個等待線程將獲得所有權。
同步事件
同步事件用於通知其他線程某件事情已發生或某個資源已可用。不要被這些使用“事件”一詞的項誤導。同步事件與其他 Visual Basic 事件不同,它們實際上是等待句柄。與其他等待句柄類似,同步事件也有兩種狀態:已發出信號和未發出信號。調用同步事件的一種等待方法的線程必須等待,直至另一個線程通過調用 Set 方法向事件發出通知。有兩種同步事件類。線程使用 Set 方法將 ManualResetEvent 實例的狀態設置為已發出信號。線程使用 Reset 方法,或者在控制返回到一個等待 WaitOne 的調用時,將 ManualResetEvent 實例的狀態設置為未發出信號。還可以使用 Set 將 AutoResetEvent 類的實例設置為已發出信號,但是只要等待線程被通知事件已發出信號,這些實例就自動返回到未發出信號狀態。
以下示例使用 AutoResetEvent 類來同步線程池任務。
Sub StartTest() Dim AT As New AsyncTest() AT.StartTask() End Sub Class AsyncTest Private Shared AsyncOpDone As New _ System.Threading.AutoResetEvent(False) Sub StartTask() Dim Tpool As System.Threading.ThreadPool Dim arg As String = "SomeArg" Tpool.QueueUserWorkItem(New System.Threading.WaitCallback( _ AddressOf Task), arg) ' 將一個任務排隊。 AsyncOpDone.WaitOne() ' 等待線程調用 Set。 MsgBox("線程運行結束。") End Sub Sub Task(ByVal Arg As Object) MsgBox("線程正在啟動。") System.Threading.Thread.Sleep(4000) ' 等待 4 秒鍾。 MsgBox("狀態對象包含字符串 " & CStr(Arg)) AsyncOpDone.Set() ' 通知線程運行結束。 End Sub End Class
監視器對象和 SyncLock
監視器對象用於確保代碼塊在運行時不會被其他線程運行的代碼中斷。換句話說,直到同步代碼塊中的代碼運行結束后,其他線程中的代碼才能運行。在 Visual Basic .NET 中,SyncLock 關鍵字用於簡化對監視器對象的訪問。在 Visual C#® .NET 中則使用 Lock 關鍵字。
例如,假設有一個反復異步讀取數據並顯示結果的程序。如果操作系統使用搶占式多任務處理技術,則可以中斷正在運行的線程而將時間用於運行其他某個線程。如果不進行同步,則如果在顯示數據時,代表數據的對象被其他線程修改,則可能會看到被部分更新的數據。SyncLock 語句可以保證代碼段在運行時不會被中斷。以下示例說明了如何使用 SyncLock 為顯示過程提供數據對象的獨占訪問權限。
Class DataObject Public ObjText As String Public ObjTimeStamp As Date End Class Sub RunTasks() Dim MyDataObject As New DataObject() ReadDataAsync(MyDataObject) SyncLock MyDataObject DisplayResults(MyDataObject) End SyncLock End Sub Sub ReadDataAsync(ByRef MyDataObject As DataObject) ' 添加代碼以異步讀取和處理數據。 End Sub Sub DisplayResults(ByVal MyDataObject As DataObject) ' 添加代碼以顯示結果。 End Sub
如果需要確保代碼段不會被在其它線程中運行的代碼中斷,請使用 SyncLock。
Interlocked 類
為避免在多個線程嘗試同時更新或比較相同的值時可能出現的問題,可以使用 Interlocked 類的方法。此類的方法使您能夠安全地遞增、遞減、交換和比較任何線程中的值。以下示例說明了如何使用 Increment 方法來遞增由在其它線程中運行的過程所共享的變量。
Sub ThreadA(ByRef IntA As Integer) System.Threading.Interlocked.Increment(IntA) End Sub Sub ThreadB(ByRef IntA As Integer) System.Threading.Interlocked.Increment(IntA) End Sub
ReaderWriter 鎖定
在某些情況下,可能希望只在寫入數據時鎖定資源,而在不更新數據時則允許多個客戶端同時讀取數據。ReaderWriterLock 類在線程修改資源時強制獨占訪問資源,但在讀取資源時允許進行非獨占訪問。ReaderWriter 鎖定是獨占鎖定的一個很有用的替代選擇,因為獨占鎖定使其他線程一直處於等待狀態,即使那些線程並不需要更新數據。以下示例說明了如何使用 ReaderWriter 來協調多個線程的讀寫操作。
Class ReadWrite ' 可以從多個線程中安全地調用 ' ReadData 和 WriteData 方法。 Public ReadWriteLock As New System.Threading.ReaderWriterLock() Sub ReadData() ' 此過程從某個來源讀取信息。 ' 讀取鎖定禁止在線程完成讀取之前寫入數據, ' 同時允許其他線程調用 ReadData。 ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite) Try ' 此處執行讀取操作。 Finally ReadWriteLock.ReleaseReaderLock() ' 釋放讀取鎖定。 End Try End Sub Sub WriteData() ' 此過程將信息寫入某個來源。 ' 寫入鎖定禁止在線程完成寫入操作前 ' 讀取或寫入數據。 ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite) Try ' 此處執行寫入操作。 Finally ReadWriteLock.ReleaseWriterLock() ' 釋放寫入鎖定。 End Try End Sub End Class
死鎖
線程同步在多線程應用程序中十分重要,但在多個線程相互等待時總是存在死鎖的危險。就象四個方向上都停有汽車的情況,每個人都在等待另一個人走,死鎖使一切操作終止。顯然,避免死鎖非常重要。有許多情況會導致死鎖,同樣,避免死鎖的方法也很多。雖然本文沒有足夠篇幅來討論與死鎖相關的所有問題,但有一點很重要,即認真規划是避免死鎖的關鍵。在開始編碼之前,通過圖解多線程應用程序,通常可以預測死鎖。
線程計時器
Threading.Timer 類對在單獨線程中定期運行任務十分有用。例如,可以使用線程計時器檢查數據庫的狀態和完整性,或者備份重要文件。以下示例每兩秒鍾啟動一個任務,並使用標志來啟動使計時器停止的 Dispose 方法。本例將狀態發送到輸出窗口,因此在測試代碼之前,應按 CONTROL+ALT+O 鍵以使此窗口可見。
Class StateObjClass ' 用於保留調用 TimerTask 所需的參數 Public SomeValue As Integer Public TimerReference As System.Threading.Timer Public TimerCanceled As Boolean End Class Sub RunTimer() Dim StateObj As New StateObjClass() StateObj.TimerCanceled = False StateObj.SomeValue = 1 Dim TimerDelegate As New Threading.TimerCallback(AddressOf TimerTask) ' 創建每隔 2 秒鍾調用過程的計時器。 ' 注意:這里沒有 Start 方法;創建實例之后, ' 計時器就開始運行。 Dim TimerItem As New System.Threading.Timer(TimerDelegate, StateObj, _ 2000, 2000) StateObj.TimerReference = TimerItem ' 為 Dispose 保存一個引用。 While StateObj.SomeValue < 10 ' 運行 10 個循環。 System.Threading.Thread.Sleep(1000) ' 等待 1 秒鍾。
轉自:http://blog.csdn.net/wuhuwy/archive/2009/04/23/4102824.aspx