開發者一直要求微軟為VB加入更多的多線程功能,對於VB.NET也是這樣。VB6已經支持建立多線程的EXE、DLL和OCX。不過使用多線程這個詞語,可能也不太確切。因此VB6僅支持運行多個單線程的單元。一個單元實際上是代碼執行的空間,而單元的邊界限制了代碼訪問任何單元以外的事物。
VB.NET就不同了,它支持建立自由線程(free-threaded)的應用。這意味著多個線程可以訪問同樣一套的共享數據。本文的以下部分將討論一下多線程的一些基本點。
問題
雖然VB6支持多個單線程的單元,不過它並不支持一個自由線程的模型,即不允許多個線程使用同一套數據。在許多的情況下,你需要建立一個新的線程來進行後台的處理,這樣可提高應用的可用性,否則,一個長的處理就可以令程序的響應變得很慢,例如你按下表格上的一個取消按鈕,卻很久都沒有響應。
解決辦法
由於VB.NET使用了CLR(Common Language Runtime),從而擁有了許多的新特性,其中的一個是可以創建自由線程的應用。
使用線程
在VB.NET中,運用線程是很簡單的。我們將在後面涉及其中的細節,現在我們首先來創建一個簡單的表格,它使用一個新的線程來運行一個後台處理。第一件要做的事情是創建運行在新線程上的後台任務。以下的代碼執行一個相當長的運行處理--一個無限的循環:
Private Sub BackgroundProcess()
Dim i As Integer = 1
Do While True
ListBox1.Items.Add("Iterations: " + i)
i += 1
Loop
End Sub
這段代碼無限地循環,並且在每次執行時為表格上的一個列表框加入一個項目。如果你對VB.NET不熟悉的話,你將會發現這段代碼和VB6的有一些區別:
. 在聲明變量Dim i As Integer = 1時賦值
. 使用+=操作符i += 1代替i = i + 1
. 沒有使用Call關鍵字
一旦我們擁有了一個工作的處理,我們就需要將這段代碼分配給一個線程處理,並且啟動它。為此我們要使用線程對象(Thread object),它是.NET架構類中System.Threading命名空間的一部分。在實例化一個新的線程類時,我們將要在線程類構造器執行的代碼塊的一個引用傳送給它。以下的代碼創建一個新的線程對象,並且將BackgroundProcess的一個引用傳送給它:
Dim t As Thread
t = New Thread(AddressOf Me.BackgroundProcess)
t.Start()
AddressOf操作符創建了一個到BackgroundProcess方法的委派對象。在VB.NET中,一個委派是一個類型安全、面向對象的函數指針。在實例化該線程後,你可以通過調用線程的Start()方法來開始執行代碼。
控制線程
在線程啟動後,你可以通過線程對象的一個方法來控制它的狀態。你可以通過調用Thread.Sleep方法來暫停一個線程的執行,這個方法可以接收一個整型值,用來決定線程休眠的時間。拿前面的例子來說,如果你想讓列表項目增加的速度變慢,可以在其中放入一個sleep方法的調用:
Private Sub BackgroundProcess()
Dim i As Integer = 1
Do While True
ListBox1.Items.Add("Iterations: " + i)
i += 1
Thread.CurrentThread.Sleep(2000)
Loop
End Sub
CurrentThread是一個public static的屬性值,可讓你得到當前運行線程的一個引用。
你還可以通過調用Thread.Sleep (System.Threading.Timeout.Infinite)來讓線程進入休眠狀態,有點特別的是,這個調用的休眠時間是不確定的。要中斷這個休眠,你可以調用Thread.Interrupt方法。
與休眠和中斷類似的是掛起和恢復。掛起可讓你暫停一個線程,直到另一個線程調用Thread.Resume為止。休眠和掛起的區別是,後者並不立刻讓線程進入一個等待的狀態,線程並不會掛起,直到.NET runtime認為現在已經是一個安全的地方來掛起它了,而休眠則會立刻讓線程進入一個等待的狀態。
最後要介紹的是Thread.Abort,它會停止一個線程的執行。在我們的那個簡單例子中,如果要加入一個按鈕來停止處理,很簡單,我們只要調用Thread.Abort方法就行了,如下所示:
Private Sub Button2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button2.Click
t.Abort()
End Sub
這就是多線程的強大之處。用戶界面的響應很好,因為它運行在一個單獨的線程中,而後台的處理運行在另外一個線程中。在用戶按下取消按鈕時,便會馬上得到響應,並且停止處理。
上面的例子只是一個相當簡單的應用。在編程時,你還需要使用到多線程的許多復雜特性。其中的一個問題是如何將程序的數據由線程類的構造器傳入或者傳出,也就是說,對於放到另外一個線程中的過程,你既不能傳參數給它,也不能由它返回值。這是由於你傳入到線程構造器的過程是不能擁有任何的參數或者返回值的。為了解決這個問題,可以將你的過程封裝到一個類中,這樣方法的參數就可使用類中的字段。
這裡我們舉一個簡單的例子,如果我們要計算一個數的平方,即:
Function Square(ByVal Value As Double) As Double
Return Value * Value
End Function
為了在一個新的線程中使用這個過程,我們將它封裝到一個類中:
Public Class SquareClass
Public Value As Double
Public Square As Double
Public Sub CalcSquare()
Square = Value * Value
End Sub
End Class
使用這些代碼來在一個新的線程上啟動CalcSquare過程,如下所示:
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim oSquare As New SquareClass()
t = New Thread(AddressOf oSquare.CalcSquare)
oSquare.Value = 30
t.Start()
End Sub
要注意到,在線程啟動後,我們並沒有檢查類中的square值,因為即使你調用了線程的start方法,也不能確保其中的方法馬上執行完。要從另一個線程中得到值,有幾個方法,這裡使用的方法是最簡單的,即是在線程完成的時候觸發一個事件。我們將在後面的線程同步中討論另一個方法。以下的代碼為SquareClass加入了事件聲明。
Public Class SquareClass
Public Value As Double
Public Square As Double
Public Event ThreadComplete(ByVal Square As Double)
Public Sub CalcSquare()
Square = Value * Value
RaiseEvent ThreadComplete(Square)
End Sub
End Class
在調用代碼中捕捉事件的方法和VB6差不多,你仍然要聲明WithEvents變量,並且在一個過程中處理事件。有些不同的是,你聲明處理事件的過程使用的是Handles關鍵字,而不是通過VB6中通常使用的Object_Event。
Dim WithEvents oSquare As SquareClass
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
oSquare = New SquareClass()
t = New Thread(AddressOf oSquare.CalcSquare)
oSquare.Value = 30
t.Start()
End Sub
Sub SquareEventHandler(ByVal Square As Double) _
Handles oSquare.ThreadComplete
MsgBox("The square is " & Square)
End Sub
對於這種方法,要注意的是處理事件的過程,在這個例子中的是SquareEventHandler,將運行在產生該事件的線程中。它並不是運行在表格執行的線程中。
同步線程
在線程的同步方面,VB.NET提供了幾個方法。在上面的平方例子中,你要與執行計算的線程同步,以便等待它執行完並且得到結果。另一個例子是,如果你在其它線程中排序一個數組,那麼在使用該數組前,你必須等待該處理完成。為了進行這些同步,VB.NET提供了SyncLock聲明和Thread.Join方法。
SyncLock可得到一個對象引用的唯一鎖,只要將該對象傳送給SyncLock就行了。通過得到這個唯一鎖,你可以確保多個線程不會訪問共享的數據或者在多個線程上執行的代碼。要得到一個鎖,可使用一個較為便利的對象--與每個類關聯的System.Type對象。System.Type對象可通過使用GetType方法得到:
Public Sub CalcSquare()
SyncLock GetType(SquareClass)
Square = Value * Value
End SyncLock
End Sub
另一個是Thread.Join方法,它可讓你等待一個特定的時間,直到一個線程完成。如果該線程在你指定的時間前完成了,Thread.Join將返回True,否則它返回False。在平方的例子中,如果你不想使用觸發事件的方法,你可以調用Thread.Join的方法來決定計算是否完成了。代碼如下所示:
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim oSquare As New SquareClass()
t = New Thread(AddressOf oSquare.CalcSquare)
oSquare.Value = 30
t.Start()
If t.Join(500) Then
MsgBox(oSquare.Square)
End If
End Sub
對於這種方法,要注意的是處理事件的過程,在這個例子中的是SquareEventHandler,將運行在產生該事件的線程中。它並不是運行在表格執行的線程中。