用VisualBasic.Net創建多線程應用程序
這篇文章假設讀者已經擁有以下的編程經驗:VB,Windows環境,基于事件的編程,基本的HTML和腳本知識。這篇文章是基于微軟.NET的Beta2版本。
VB.NET的其中一個最令人期待的特性是可以創建和管理線程。雖然在VB6的應用中,我們可以通過Win32 CreateThread API來創建一個多線程的應用,或者通過欺騙COM庫在一個獨立的線程中創建一個組件,不過這些技術都是難以調試和維護的。
造成這些困難的主要原因是由于VB 6.0并不是用來處理多線程應用的,這樣會導致訪問違例和內存錯誤。不同的是,Common Language Runtime(CLR)是為多線程的環境設計的,實際上Services架構在基本委派體系中就暗中集成了這個功能。其實,通過使用System.Threading命名空間,Services架構還支持顯式使用線程API。
對于那些不熟悉線程的讀者,這里簡單介紹一下,它可讓你的應用分成多個單元執行,這些單元都是被搶先型的操作系統(例如Windows 2000)分配在不同時間運行,并且擁有不同的優先權。根據線程的優先權和特別的調度算法,操作系統分配每個線程運行一段的時間,稱為time slice。當這段time slice過去時,線程就會掛起并且放回到隊列中,接著另一個線程又會被分配一段time slice運行。在線程掛起時,它的狀態就會被保存下來,以便下一次可以由停下來的地方開始工作。CLR支持線程的方式是通過啟動每個帶有一個主線程的AppDomain,并且允許它創建多個工作線程,每個工作線程都擁有自己的例外處理和狀態數據。
在一個應用中使用超過一個線程的明顯好處是你的應用看來正在同時執行幾個任務,這是由于不同的線程都得到了CPU的運行時間。實際上,在一臺擁有多個處理器的機器上,來自一個AppDomain的線程可分配在所有的處理器上運行,從而允許同時地運作。在分布式的應用時,這樣可提升擴展性,因為更多的客戶可以分享一個服務器上的CPU資源,而對于桌面的應用,例如電子表格和word等也能夠從線程中得到好處,可執行后臺的操作例如重新計算和打印。不過,在使用VB.NET寫分布式的應用時,如何應用這個概念呢?
對于初學者,在你建立分布式的應用時,實際上你已經使用了一個多線程的體系。這是由于應用服務,例如IIS、組件服務和SQL Server全部都是多線程的。例如,在客戶端請求網頁時,它們的請求被由IIS控制的工作線程運載。這些線程中的其中之一可能會執行一個ASP.NET頁面,該頁面會調用組件服務中的一個組件。組件應該被配置為作為Server應用運行,這樣它就會被該應用的一個線程池中的一個線程執行。組件也可能使用一個數據庫連接,該連接是由SQL Server引擎分配的工作線程池中得到的。結果是,多個用戶請求網頁,要初始化組件和訪問數據庫時,它們的活動并不是連續的,因此不會受到單線程執行的限制。
由于你在分布式的應用中所寫的大部分代碼都是在中層執行的,因此有些情形你需要顯式地創建線程。這些情形包括有長時間的操作,例如文件IO,數據庫維護任務,在一個Windows服務應用中為多個客戶服務,以及由一個Microsoft Message Queue監聽信息。這里只是會為你介紹使用線程的一些基本點,要得到更多的信息和其它的例子你可以查看其它的文檔。
要注意的問題:由于操作系統要跟蹤和確定線程的進度,因此線程的系統開銷會比較大,因此你不應該在應用的任何地方都創建新的線程。由于必須為每個線程分配內存,太多的線程將會令整個系統的性能受到影響。此外,線程還會帶來一些VB的開發者沒有遇到過的問題,例如同步訪問和同享資源。因此,你必須經過仔細考慮才加入多線程的支持。
在下面的部分,我們將會討論使用線程和線程池。
使用線程
用來創建和維護線程的基類是Thread。它擁有Start, Stop, Resume, Abort, Suspend和Join (wait for)等方法讓你操縱線程,還可以通過如Sleep, IsAlive, IsBackground, Priority, ApartmentState和ThreadState等方法查詢和設置線程狀態。
注意:要記住大部分的Thread成員都是虛成員,因此只可以由一個特定Thread類的實例訪問。要維護一個特定的線程,你可以創建一個新的Thread類實例,或者通過CurrentThread屬性得到當前Thread的一個引用。例外的是Sleep方法,它可讓當前的線程掛起指定的毫秒數。
為了啟動一個新的線程,你必須指定一個入口以便開始執行該線程。要求是該方法(可以是一個對象上的方法或者是一個模塊中的方法)沒有參數,并且要定義為一個Sub過程。在同一個對象內,以一個獨立的線程來執行一個方法也是可能的。
例如,看以下的代碼段。在這個例子中,Instructors類的GetPhotos方法在一個獨立的線程上執行。這個方法(沒有顯示)向數據庫查詢全部的教師圖象,并且將每幅圖象以文件的方式保存下來,在這里,數據庫訪問和文件訪問在一個分開的線程上執行。
| Dim tPhoto As Thread Dim tsStart As ThreadStart Dim objIns As New Instructors tsStart = New ThreadStart(AddressOf objIns.GetPhotos) tPhoto = New Thread(tsStart) tPhoto.Priority = ThreadPriority.BelowNormal tPhoto.Name = "SavingPhotos" tPhoto.Start() ' Wait for the started thread to become alive While (tPhoto.ThreadState = ThreadState.Unstarted) Thread.Sleep(100) End While ... If tPhoto.IsAlive Then MsgBox("Still processing images...") MsgBox("Waiting to finish processing images...") tPhoto.Join End If MsgBox("Done processing images.") |
在上面的代碼中,你可以看到啟動一個線程包括實例化一個ThreadStart委派,并且通過AddressOf操作符將入口地址傳送給它。該委派然后就會傳送給Thread類的構造器。在線程真正開始執行前,優先權被設置為BelowNormal,這樣主線程將可更迅速地響應請求。雖然Win32 API支持30個優先權級別,不過在ThreadPriority枚舉中,你只有4個其它的優先級可以設置 (AboveNormal, Highest, Lowest和Normal) 。
注意:ThreadPriority枚舉對象和Win32 API的32個級別是有對應關系的,實際上,最低的優先權(Lowest)對應6,而最高的(Highest)為10。
然后代碼就設置了線程的Name屬性,開始看來有點奇怪,因為一個線程或者是它的名字應該永遠都不會在用戶的界面上出現,這個名字其實是出現在調試器中,也可用作日志的用途。接著就是執行Start方法來真正開始執行。
技巧
有時得到線程的一個數字標識來作日志和匯報目的是非常方便的。你可以調用CurrentThread屬性或者Thread類上的GetHashCode方法。這將會返回一個數字,你可以用它來在應用中作記錄或者事件日志。
啟動線程后,代碼就進入一個循環等待,檢查ThreadState屬性的值是否為Unstarted(這是線程的初始狀態),直到線程啟動。ThreadState枚舉還包括有9個其它的狀態,由Running到Stopped。要注意的是調用Thread類的共享方法Sleep將會令該線程休眠指定的毫秒數,在這里是主線程而不是tPhoto表示的線程。最后,在執行一些其它的工作后,主線程通過檢查IsAlive屬性來看tPhoto是否仍然運行。如果是的話,就會在調用Join方法前,向用戶展示相應的信息。該方法通過阻塞來同步兩個線程(掛起當前執行的線程)。直到調用該方法的線程停下來為止。
技巧
與上面提到的Priority屬性無關,CLR會區分前臺運行的線程和后臺運行的線程。如果一個線程被標識為后臺線程,CLR在AppDomain關閉的時候并不會等待它完成。如前面討論的那樣,在使用異步文件IO時,運行時創建的線程都是后臺的線程,因此你要確保代碼的主線程不會在I/O完成前退出。默認的情況下,上面創建的線程被標識為前臺,同時它們的IsBackgropu屬性被設置為False。
雖然在代碼中并沒有展示,不過在線程執行的時候它可以通過Suspend方法掛起,然后通過Resume繼續執行。此外線程還可以通過使用Abort方法退出,這時將會在線程內拋出一個例外。
對資源的同步訪問
一般來說,你希望在獨立的線程中運行各種處理,而不需要訪問共享的資源。建議的方法如下:
1、封裝要運行的處理到一個類中,并且留一個入口來啟動該處理,例如Public Sub Start()并且初始化變量來處理狀態
2、創建一個獨立的類實例
3、設置處理需要的實例變量
4、在一個獨立的線程中調用入口
5、不要引用該類的實例變量
只要使用這個方法,全部的實例變量對于線程都是“私有的”,因此可以無需擔心同步的問題。
不過,有時這種情況是不能避免的,例如數據庫連接或者文件處理。為了確保某線程在訪問這些資源時其它線程處于等待狀態,你可以使用Monitor類和它的相關方法,包括有Enter, Exit, TryEnter, Wait, Pulse和PulseAll。
例如,假定上面代碼中的Instructors類包含了一個類級的SqlConnection對象,該對象被所有的方法共享,并且用來連接數據庫。這就是一個資源共享的例子,它被類中的所有方法所共享。
注意:
雖然使用連接池可提供一個更富擴展性的方案,不過這個例子滿足我們當前的需要,它讓所有的數據庫訪問通過一個單一的數據庫連接進行。這種方式對于需要一個持久的數據庫連接的應用是適合的,不過不適合用在分布式的應用。
這個例子中,我們假設在調用GetPhotos后,客戶端繼續調用一個使用該連接對象的方法。由于連接可能正在被GetPhotos使用,如果SqlConnection正在忙于處理其它的結果,該方法將會拋出一個例外。
要避免這種情形,GetPhotos方法可以使用Monitor的共享方法在其代碼中創建critical section。簡單說來,critical section就是調用Monitor類的Enter和Exit方法所構成的代碼塊,通過它,訪問的同步是基于傳送至Enter方法的對象。也就是說,如果GetPhotos方法要獨立地使用SqlConnection,它必須要創建一個critical section,在該section的開始部分,通過傳送SqlConnection到Monitor的Enter方法中,并且在結束的時候調用Exit方法。被傳送的對象可以是任何繼承System.Object的對象。
如果該對象正在被其它的線程使用,Enter方法將會阻塞直到對象被釋放。你也可以調用TryEnter方法,該方法不會阻塞,它只會返回一個布爾值指示該對象是否在使用中。一旦進入critical section,GetPhotos方法可以使用SqlConnection執行一個存儲過程,并且將結果寫出來。在關閉結果集SqlDataReader后,就會調用Monitor類的Pulse方法,以通知等待隊列中的下個線程該對象已經釋放了。然后就會將線程移動到ready隊列中,以便準備開始處理。PulseAll方法則通知全部的等待線程該對象準備被釋放。最后就會調用Exit,從而釋放monitor并且結束critical section部分。這部分代碼的框架見下。
同步的資源。以下的例子展示了GetPhotos方法將使用Monitor類來確保兩個線程不會同時使用SqlConnection對象
| Public Sub GetPhotos() Dim cmSQL As SqlCommand Dim sdrIns As SqlDataReader Try ' Execute proc cmSQL = New SqlCommand("usp_GetPhotos", mcnSQL) cmSQL.CommandType = CommandType.StoredProcedure ' Enter critical section Monitor.Enter(mcnSQL) ' Alternate code ' Do While Not Monitor.TryEnter(mcnSQL) ' Thread.CurrentThread.Sleep(100) ' Loop sdrIns = cmSQL.ExecuteReader() Catch e As Exception End Try Do While sdrIns.Read ' Read the data and write it to a binary stream Loop sdrIns.Close Monitor.Pulse(mcnSQL) Monitor.Exit(mcnSQL) ' Exited critical section Return End Sub |
很明顯,critical sections僅應該在需要的時候創建,因為它們會阻塞線程,從而會影響整體的吞吐量。
要同步線程間共享的實例變量,有一個很簡單的技巧,這就是使用Interlocket類。該類包含有共享的Increment和Decrement方法,可以將修改變量和檢查結果的操作結合成一個單一的操作。這樣做是必需的,因為一個線程可以修改變量的值,在接著檢查結果之前,它的運行時間就結束了。在該線程再次運行時,變量的值就有可能被其它的線程修改了。
例如下面的代碼增加Instructors類的mPhotosProcessed實例級變量的值:
| Interlocked.Increment(mPhotosProcessed) |
Interlocked類還支持Exchange和CompareExchange的方法,它們的作用分別是設置變量為特定的值,或者在該變量等于某個值時才這樣做。
使用線程本地存儲
雖然在理想的情況下你的線程將使用私有的實例變量,不過在許多時候,當你的線程運行一個對象的方法,而該方法可能被其它的線程共享時,這樣你的線程可能需要存儲和接收它自己的真正私有數據。例如,當一個線程池中的線程監視一個MSMQ隊列,并且需要取得隊列中數據,然后存儲下來作以后處理用時,就會出現這種情形。
在Windows操作系統中,每個線程都擁有自己的線程本地存儲(thread local storage,TLS),以用來跟蹤狀態信息。方便的是,Thread類擁有一套方法,可方便地創建和維護TLS中的內存區域(該區域稱為data slots)。
值得一提的是,Thread類擁有一個共享的AllocateNamedDataSlot方法,可以使用指定的名字為AppDomain中的所有線程創建一個新的data slot。該slot可以在隨后通過使用SetData和GetData方法設置和讀取。例如,假定有一個稱為WorkerClass類執行一些處理活動,并且我們想創建一定數量的線程來執行該工作。以下的代碼段為所有的線程創建了一個稱為“ID”的data slot,然后通過objWorker實例的StartWork方法,執行相應數量的線程:
| Dim dssSlot As LocalDataStoreSlot Dim tNew As Thread Dim objWorker As WorkerClass dssSlot = Thread.AllocateNamedDataSlot("ID") For i = 0 to intMaxThreads tNew = New Thread(New ThreadStart(AddressOf objWorker.StartWork) tNew.Start Next |
要注意的是由于所有的新線程將會共享objWorker上的實例變量,因此StartWorker方法和任何通過Start調用的方法將需要使用同步以防止對這些變量的同時訪問。不過,如果每個線程需要它們自己的數據在方法間共享,它們可以將一個拷貝放到TLS的“ID”slot中,如下所示。
| Public Sub Start() Dim dssIDSlot As LocalDataStoreSlot Dim myID As Integer ' Do other work dssIDSlot = Thread.GetNamedDataSlot("ID") Thread.SetData(dssIDSlot, myID) Call NextProcess() End Sub Private Sub NextProcess() Dim myID As Integer Dim dssIDSlot As LocalDataStoreSlot dssIDSlot = Thread.GetNamedDataSlot("ID") myID = Thread.GetData(dssIDSlot) ' Do other work End Sub |
當NextProcess方法被調用時,數據可以再次通過使用Getdata由slot中讀取。
再次提醒一下,上面提到的設計模式在需要時才使用。只有在你的設計是很復雜而且需要從多個線程中訪問同樣的對象時,你才需要使用TLS。
使用線程工具
你可以通過Thread類來創建和管理自己的線程,System.Threading命名空間還提供了一個簡單的方式來使用線程,這些線程由CLR分配的一個池得到。這樣做是可能的,因為CLR自動在每個進程創建和管理一個線程池,這樣做是為了用來處理異步的操作,例如I/O和事件。在池中,一個線程被分配Highest優先權利,它是用來監視隊列中其它線程的狀態的。使用ThreadPool類,你的代碼可接進這個池,并且可以更有效地使用這個在運行時已經配置的體系。實際上,ThreadPool類可允許你提交工作項目(例如要執行的方法)到池中,它們會被隨后的工作線程執行。
如前所述,只有在應用需要的時候才使用線程,并且要經過仔細的分析。例如,使用線程池的一個很好的情形是,一個用來監聽由一個或者多個信息隊列中進入的新信息的Windows服務應用。雖然System.Messaging命名空間支持異步的操作,但是創建一個線程池可允許你控制一些特別的方面,例如有多少線程在處理信息和線程的生存時間。
下面例子是一個經過簡化的類,它使用ThreadPool類,用來監聽一個MSMQ隊列。
列表11.9 QueueListener類,該類使用ThreadPool類來監聽一個MSMQ隊列
| Option Strict Off Imports System Imports System.Threading Imports System.Messaging Imports Microsoft.VisualBasic Public Class QueueListener ' Used to listen for MSMQ messages Protected Class EventState ' Used to store the event and any other state data required by the listener Public ResetEvent As ManualResetEvent Public ThreadName As String Public Overloads Sub New(ByVal myEvent As ManualResetEvent) MyBase.New() ResetEvent = myEvent End Sub Public Overloads Sub New(ByVal myEvent As ManualResetEvent, ByVal Name As String) MyBase.New() ResetEvent = myEvent ThreadName = Name End Sub End Class Private mstrMachine As String Private mstrQueue As String Private mWorkItems As Integer = 7 Private mFinished As Boolean = False Dim mEvs() As ManualResetEvent Public Property WorkItems() As Integer Get Return mWorkItems End Get Set(ByVal Value As Integer) If Value > 15 Then mWorkItems = 15 Else mWorkItems = Value End If End Set End Property Public Sub New(ByVal Machine As String, ByVal Queue As String) ' Constructor accepts the necessary queue information mstrMachine = Machine mstrQueue = Queue End Sub Public Sub Listen(ByVal state As Object) ' Method that each thread uses to listen for messages ' Create a MessageQueue object Dim objMQ As System.Messaging.MessageQueue = New System.Messaging.MessageQueue() ' Create a Message object Dim objMsg As System.Messaging.Message ' = New System.Messaging.Message() ' Event from the state Dim evs As ManualResetEvent ' Cast the state into the event evs = state.ResetEvent ' Set the priority and name Thread.CurrentThread.Priority = ThreadPriority.BelowNormal Try If Not state.ThreadName Is Nothing Then Thread.CurrentThread.Name = state.ThreadName End If Catch e As Exception ' Thread name can only be set once ' Don't set it and get out End Try 'Console.WriteLine("Listen {0} ", state.ThreadName) Try ' Set the path property on the MessageQueue object, assume private in this case objMQ.Path = mstrMachine & "private$" & mstrQueue ' Repeat until Interrupt received While True Try ' Sleep in order to catch the interrupt if it has been thrown Thread.CurrentThread.Sleep(100) ' Set the Message object equal to the result from the receive function ' Will block for 1 second if a message is not received objMsg = objMQ.Receive(New TimeSpan(0, 0, 0, 1)) ' Message found so signal the event to say we're working evs.Reset() ' Processing the message ProcessMsg(objMsg) ' Done processing Catch e As ThreadInterruptedException ' Catch the ThreadInterrupt from the main thread and exit Exit While Catch excp As MessageQueueException ' Catch any exceptions thrown in receive ' Probable timeout Finally ' Console.WriteLine("Setting Event " & Thread.CurrentThread.GetHashCode()) ' Done with this iteration of the loop so set the event evs.Set() End Try ' If finished then exit thread If mFinished Then 'console.WriteLine("exiting " & thread.CurrentThread.GetHashCode) Exit While End If End While Catch e As ThreadInterruptedException ' Catch the ThreadInterrupt from the main thread and exit End Try End Sub Private Sub ProcessMsg(ByVal pMsg As Message) ' Here is where we would process the message End Sub Public Sub Monitor() Dim intItem As Integer Dim objState As EventState ReDim mEvs(mWorkItems) mFinished = False 'Console.WriteLine("Queuing {0} items to Thread Pool", mWorkItems) For intItem = 0 To mWorkItems - 1 'Console.WriteLine("Queue to Thread Pool {0}", intItem) mEvs(intItem) = New ManualResetEvent(False) objState = New EventState(mEvs(intItem), "Worker " & intItem) ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf Me.Listen), _ objState) Next End Sub Public Sub Finish(Optional ByVal pTimeout As Integer = 0) 'Console.WriteLine("Waiting for Thread Pool to drain") ' Make sure everyone gets through the last iteration mFinished = True ' Block until all have been set If pTimeout = 0 Then WaitHandle.WaitAll(mEvs) ' Waiting until all threads signal that they are done. Else WaitHandle.WaitAll(mEvs, pTimeout, True) End If 'Console.WriteLine("Thread Pool has been drained (Event fired)") End Sub End Class |
要注意該列表包含有兩個類:EventState,它是一個protected的子類,還有QueueListener。EventState包含有一個稱為ResetEvent的字段,它的類型是ManualResetEvent,用來確保所有的工作線程可以無中斷地完成它的工作,這是通過使用ResetEvent字段得到其狀態。該類還包含有一個ThreadName字段,用來設置與該類相關的線程的名字,以便作調試用。
技巧
下圖展示了VS.NET在調試模式時運行這個多線程監聽應用的情形。要注意的是下拉的窗口顯示了每個線程的名字。選擇線程后,代碼窗口就會移動到該線程正在執行的地方。要注意的是一個線程的名字只可以設置一次。因此,當工作項目使用同一個線程時,如果設置了Name屬性,代碼就會拋出一個例外。
|
QueueListener是真正由多個線程上取回MSMQ隊列的類,它還包含有一個構造器,該構造器接收機器名字,并且以隊列的形式將名字送至監視器。public Listen方法由隊列中接收信息,而public Monitor方法初始化處理并且創建線程池。private ProcessMsg方法則是用來處理接收信息的。最后是public Finish方法,它可以接收一個超時參數,可讓QueueListener類使用的線程在一個指定的時間內完成工作。
首先,要注意到Listen方法接收一個狀態對象作為參數。該對象將包含有一個EventState的實例,該實例將被Listen用來檢查該方法是否正在處理信息還是已經完成處理。通過這樣做可確保Finish方法阻塞直到所有的線程完成它們當前的處理。在設置ThreadPriority和Name,以及接收EventState后,你將會注意到該方法僅包含有一個放在Try塊中的While循環。該循環反復調用MessageQueue類的Receive方法,方法將返回在指定的超時時間內的第一個得到的信息。如果沒有信息,在返回前,就會使用一個TimeSpan對象來通知Receive方法阻塞一秒。如果沒有信息接收,將會拋出一個MessageQueueException對象。要注意的是如果有信息到達,該方法將會繼續運行并調用Reset方法,Reset方法屬于EventState對象內的ResetEvent字段。無論是哪種情況,Finally塊都會調用ResetEvent字段的Set方法,表示線程已經完成這個循環處理。
前面已經提及,EventState的ResetEvent字段包含有一個ManualResetEvent的實例,該實例是一個事件對象,它的signaled和non-signaled狀態都是可以通過Reset和Set方法手工修改的。在調用Reset方法時,狀態就會變為non-signaled,它表明該線程正忙。當狀態通過Set事件設置為signaled時,則表明該線程已經完成處理,因此可以安全地破壞。
其中有意思的部分是由Monitor方法完成的。在這個方法中,會創建一個類級別的ManualResetEvent數組,該數組的大小和池將要服務的工作項目的數目一樣。
注意
要記住的是,在這篇文章中,工作項目和線程并不是一件事情。工作項目是由線程完成的,但是在應用中,工作項目的數目可以比線程更多。當前runtime支持的線程池大小是30,因此如果提交超過30個工作項目到池中將自動令一些工作項目必須等待其它的工作項目完成。在這個例子中,如果有超過30個工作項目的話,那將永遠不會運行,因為每個工作項目調用Listen,它將一直控制線程,直到Finish方法被調用。因此,為了確保runtime還有其它的線程作其它用途,WorkItems(工作項目)不要超過15個。
工作項目的數目可以通過QueueListener類的WorkItems屬性設置,它的默認值是7。接著就會通過一個For循環來創建每個ManualResetEvent對象,并且將它們和一個新的EventState相聯系。然后結果對象objState就會作為第二個參數傳送到ThreadPool類的共享方法QueueUserWorkItem中。就象它的名字隱含的意思一樣,該方法令工作項目以隊列的形式送給runtime管理的線程池,以等待下一個工作線程完成它。第一個參數是用來指定在工作項目開始執行的時候需要回調的方法,在這里是Listen。通過傳送EventState作為第二個參數,Listen方法可以接收該對象,并且如我們前面討論的一樣,使用里面的狀態信息。在這里,狀態包含有用來調試的線程名字和一個用來同步線程的ManualResetEvent對象。在循環完成后,指定數目的工作項目將會以隊列的形式被線程池執行。此時線程將會不斷地檢查指定的隊列以得到新信息。
在客戶端最終調用Finish方法來完成執行時,首先會設置其private mFinished變量為True。Listen方法在每次循環時都會檢查該變量,如果設置為True時,就會退出循環,釋放線程并且返回到池中。接著Finish方法將會使用WaitHandle類的共享WaitAll方法阻塞,直到mEvs數組中的所有ManualReset事件對象都被設置為signaled狀態(True)。如果超時值被傳送給該方法時,就會使用可選的第二個參數,在反阻塞現有的線程前,會等待指定的時間。使用這種方法,就可以確保Finish方法將一直阻塞,直到每個工作線程已經完成Listen方法中的當前循環。值得一提的是,線程確實被返回到池中而沒有被破壞。這樣在下一次調用Monitor時,將會重新使用現有的線程,不會重新創建它們而帶來系統開銷。
對于使用QueueListener的客戶來說,其實現如下面的代碼所示:
| Dim objQ As New QueueListener("ssosa", "tester") objQ.WorkItems = 10 objQ.Monitor() ' Do other work here objQ.Finish() |
在初始化一個新的對象,并且傳送它監聽的機器名和隊列,工作項目的數字就被設置好,并且調用Monitor方法。其后,客戶端可以調用Finish方法來清除工作線程(可帶超時參數)。
這個例子向你解釋了如何使用ThreadPool類,不過它當然不是創建線程池以執行監視消息隊列的唯一方法。例如,可以很容易地修改QueueListener來創建和跟蹤類中的Thread對象數組,以實現線程池。接著Finish方法在設置mFinished標志后,就可以執行一個循環來監視IsAlive屬性,以決定線程池何時耗盡,這時就無需使用ManualResetEvent對象了。此外,上面提到TLS技巧可以用來傳送狀態信息給線程。這個體系可讓你更好地控制線程,實際上,當runtinme管理的線程已經很繁重或者需要更多的工作項目時,這個方法將是更好的。