## CSDN博客

### 介绍VB.NET的线程(英文)

One of the most notable new features in VB.NET is the ability to create threads in your application. Visual C++ developers have been able to write multithreaded code for years, but achieving the same effect in VB6 was fraught with difficulty.

Although this exercise uses VB.NET code, there's no reason why you can't get the same results using C#.

The first question we need to answer is "what is a thread?" Well, put simply, a thread is like running two programs in the same process. Every piece of software you've written thus far contains at least one thread - the primary application thread.

For the uninitiated, a process is effectively an instance of a running program on your computer. Say you're running both Microsoft Word and Microsoft Excel. Both Word and Excel both run in a separate process, isolated from each other. With Windows 2000, there is also a collection of other programs that run in the background providing things like USB support, network connectivity, and so on. These are called "services", and each one of those runs in its own service too.

A classic example of multithreaded usage is Microsoft Word's spell checker. One thread (the primary application thread) allows you to enter text into your document, another thread runs constantly and watches what you type, checking for errors as you go and flagging problems with spelling.

The reason for using threads is simple - it improves the performance of your application, or rather it improves the user experience. Modern computer systems are designed to do many things at once, and, to use our Microsoft Word example again, keeping up with whatever you're typing isn't difficult for it. In fact, Word has a lot of spare processing capacity because it can work so many times faster than you or I can type. By introducing threads that can do other stuff in the background, Word can take advantage of the spare capacity in your computer and make your user experience a little more pleasurable.

Another example is Internet Explorer. Whenever IE has to get a resource, such as a Web page or image, from the Internet, it does so in a separate thread. The upshot of this is that you don't have to wait for IE to get an entire page before it will display the page for you. For example, it can download the HTML that makes up the text of the page in one hit, use the primary application thread to show you what it has so far and then it can start up multiple threads to go away and download each image that's referenced on the page. You can still scroll up and down the page despite the fact that it's still busy getting the rest of the data.

So, as a .NET developer, do you have to use threads? If you develop desktop applications, absolutely, because you'll easily find many ways that your UI can be improved through threads. If you develop server applications, there is scope for using threads, but not every job is appropriate.

One classic server application example is collating information from diverse data sources. Imagine you build a method on an object that needs to collect responses from five or six Web Services dotted around the Internet. Each of those Web Services will have a certain response time, depending on how busy the server is, how far away it is (in terms of the quality of the link) and what it has to do to get the data. You cannot return from the method until you've correlated all of the responses from each of the servers.

Without threading, you have to do this job sequentially, i.e. you ask the first one, wait for a response, ask the second one, wait again, and so on. With threading, you can make all the operations sequential by making all six requests at the same time, and then collate information when all the requests have been satisfied. That way, the caller has to wait for the "longest response time", rather than an aggregate of all six of the response times.

### Synchronization

If you're a reader who's never written multithreaded code before, you might be thinking, "This doesn't look hard at all!" Well, there is a wrinkle that can make writing stable multithreaded code very difficult indeed.

As you know, Windows is a multitasking operating system, which means it can do many things at once. Traditionally this means that Windows can run many processes at once, and indeed it can. If you're using a Windows 2000 computer to read this, you have a whole slew of services running in the background, Internet Explorer, a mail client and perhaps more applications in the foreground. Each of those individual programs represents a process.

The vast majority of multitasking operating systems operate process isolation, which is a strategy whereby processes running on the same computer are not allowed to interfere with each other. This interference can either be accidental (e.g. shoddy code), or deliberate. Process isolation is achieved through preventing processes from accessing memory allocated to another process.

For example, it's not possible to write a piece of software that monitors the block of memory that Internet Explorer uses to store the HTML making up a Web page. If you could, when a secure Web page was downloaded, you could go away and copy the memory blocks storing that page somewhere else. Windows prevents developers from writing code that reads directly from or writes directly to another processes memory.

What this means is, if you're used to a single threaded application, only one part of your program is ever trying to access memory you're using. Imagine you have a class and define a member variable on it. If you want to read or change that value, no one else is going to be reading or changing that same value at the same time.

This is not the case with multithreaded software, and is where the rather heady concept of thread synchronization comes in. In effect, what you're doing is sharing memory with another "process", so if you want to change a variable, you need to make sure that no one else is using it.

#### Locking and Wait States

Here's some code:

Dim a As Integer
a = m_MyVariable
a += 1
m_MyVariable = a


Imagine that you have two threads trying to use this code, and that m_MyVariable is set to 1 on object initialisation. What we're doing is rather than using one thread to add 2 to m_MyVariable to get 3, we're using two threads, each adding 1, so we will still end up with a result of 3. We have a problem if, by the time both threads have finished executing, m_MyVariable is not 3.

In this scenario, we have two threads trying to access and change m_MyVariable. If both threads hit this code at the same time, both threads will get a value of 1 from m_MyVariable and store it. Both threads then add 1 to a, and set the result back into m_MyVariable. In this scenario, when both threads have finished their work, m_MyVariable is 2, so our algorithm hasn't worked.

What we need to do is synchronise access to the block of code and make sure that only one of the two threads has access to it at any one time. Simply, we put a lock around the code and only allow one thread to open the lock and go through the code. Any other threads trying to open the lock have to wait (we say they are "blocked") until the first thread releases the lock, in which case one of the waiting threads will be able to open it, and so on.

This kind of locking is known as a "critical section", and isn't super-efficient because you're creating bottlenecks when you don't need to. Imagine this code:

Dim a As Integer
a = m_MyVariable
MsgBox("The value is " & a.ToString)


If you put a critical section lock around this code, only one thread will be able to access it at any one time. Well, from our perspective, what's the harm in having both threads access it at any one time? Neither thread is changing anything - they're both just trying to read a value. Using a critical section in this case creates a bottleneck for no reason, and removes the advantage of using threading at all!

Imagine we have another thread that writes to the value. Well, in this case, we need exclusive access to the block of code. We don't want anyone trying to read a value we're about to change, and we don't want anyone else to write to it either. In this case, we ask the thread to open the lock for "writing".

In both cases, if we want to "read", if someone else is "writing", we'll be blocked until the write lock is released. Likewise, if we want to "write", if someone else is "writing" or "reading", we'll be blocked until all the locks have been released.

A "reader/writer lock", then, provides a more meaningful locking mechanism that will only create bottlenecks when appropriate.

Although our example isn't very meaningful, you must think hard about synchronisation whenever you're using threading. In Visual C++, writing code that didn't lock and block properly would oftentimes result in horrendous crashes. The good thing is that with C++, you're writing at such a low level that the crash would often point out where you'd gone wrong. In VB.NET, you're working at a higher level and so the chances of crashing are smaller as VB will try and handle most problems for you. When it does this, something will "not work properly" as opposed to "explode", and you'll have to dig around harder to find the cause of the problem. Finding problems caused by improper synchronisation is more difficult in VB.NET, so put more thinking time in at the beginning!

### An Example

In this article, we're going to see an example of how to create a simple desktop application that uses threads to count the number of characters and number of words in a block of text. It will, eventually, look like this:

All of the threading functionality in .NET is implemented in the System.Threading namespace. Specifically, the object we need to use to create a thread is the System.Threading.Thread object.

As both of these classes have a lot in common (they both need access to a block of text, they both need a way of reporting the results to the user and they both need to be controlled from the primary application thread), we're going to create a separate class called MyWorkThread and derive WordCounter and CharCounter from this.

The other important thing about this is that we only want to create each thread once. Creating threads has an amount of overhead and so we want to do it as rarely as possible. (.NET does support thread pooling, but that's beyond the scope of this discussion.)

Whenever a thread is being blocked, it drops into a super-efficient wait state. This means it has virtually no effect on processor usage at all - to all intents and purposes it doesn't exist. So, we can have as many threads as we like, without compromising the performance of the computer. (This is an over simplification, but is roughly correct.)

As our text won't be changing constantly (or, if it is, we can actually process the text faster than the user can type), we don't want our threads to be running all the time. We want them to spend most of their life asleep, and wake up to do their job on demand. We'll be building functionality into our base MyWorkThread class to do this.

#### Creating the Project

To create the project, open VS.NET and start a new Visual Basic- Windows Application project. Our example here is called DesktopDemo, and you might care to use that name to keep things consistent.

#### Creating the Form

As you can see from our earlier screenshot, we're going to create a grid of text box controls that reports the status of each of the threads. You might have guessed that we're going to create a control array to handle this. However, when I was building this application, I couldn't work out how to create a control array using the VB.NET designer, so I ended up writing code that programmatically created controls on the form. I decided to stick with this method in this exercise, as I figured a discussion on dynamic control creation might be quite useful.

To kick off, here is the form as it stands without the extra controls. The controls are named txtText, cmdStartThreads and cmdStopThreads.

Our first job is to add a member variable to the form that will hold an array of the controls:

Public Class Form1
Inherits System.WinForms.Form

' create somewhere to keep track of all the threads...
Const NUMTHREADS As Integer = 16


As you can see, we're using a constant called NUMTHREADS to hold the number of threads. An interesting point is, thanks to our method of creating the text boxes on the fly, changing this value will automatically change the UI, which is neat.

To build the basic form, VB calls a function called InitializeComponent. This creates the four basic controls on our form, and defines the size and position of the form. (To see this method, expand the Windows Form Designer generated code region contained within the class.)

After InitializeComponent has returned, we can create our dynamic controls. Our first job is to look at the design of the form and use that to estimate where the other forms go. Specifically, we're going to use the gap between the text box and the buttons as a metric to determine where the rest of the controls go. Add this code:

Public Sub New()

    MyBase.New()

Form1 = Me

'This call is required by the Win Form Designer.
InitializeComponent()

'TODO: Add any initialization after the InitializeComponent() call

' work out the distance between the bottom of the text box, and the top
Dim margin As Integer


Good news for VB6 desktop developers: in VB.NET, the crazy twip has been dumped and we now use pixels. This means we no longer have to mess around with converting twips to pixels and back again.

After we have the margin, we need to work out the y-coordinate of the first dynamic control:

    ' then, start out new controls at the bottom of the start button, plus a
' small border...
Dim y As Integer = cmdStartThreads.Bounds.Bottom + (margin * 4)


After this, we need to work out how wide our controls should be. We want two columns, so we need to base this on the width of the form, and use our margin value to space everything properly.

    ' next, work out how wide the form is...
Dim width As Integer = Form1.Bounds.Width

' then, work out the mid point of the form, and the width of
' any controls, given you want 2x margin on both sides...
Dim midpoint As Integer = (width / 2).toint32
Dim controlwidth As Integer = midpoint - (margin * 4)
Dim controlheight As Integer = 20


Once we have the metrics, we can start looping and creating the TextBox controls. We'll also put a default value in the box indicating the ID of the thread that will be using it:

    ' create an array of text box controls...
Dim n As Integer
For n = 0 To NUMTHREADS - 1

' create a new object...


Next we need to work out whether to put the control on the left or on the right. We use Mod to determine if n is even or odd.

        ' work out where to put it...  even numbered controls go
' on the left, odd on the right...
If n Mod 2 = 0 Then
m_threadoutputs(n).location = New System.Drawing.Point(margin * 2, y)
Else
End If


VB won't know it needs to manage the control unless we add it to its Controls array:

        ' finally, add the object to the list of controls on the form...


Our last job is to adjust the y co-ordinate if we just added an odd number control and then close the loop:

        ' then, work out the next y co-ordinate for the control if we're an
' odd numbered control...
If n Mod 2 = 1 Then y += 20 + margin

Next

End Sub


If we run the code now, we'll see something like this:

Now we can look at actually creating the threads!

MyWorkThread is going to need the following controls:

• m_Pause - an instance of a System.Threading.ManualResetEvent object that lets us pause and resume the thread,
• m_IsCancelled - a Boolean flag telling is if the thread has been cancelled,
• m_Control - a reference to the TextBox control we create dynamically on the main form.

Imports System
Imports System.WinForms


Then, add references to the member variables to the class definition. Also, add the keyword MustInherit to the class definition. This means we won't be able to ever instantiate an instance of MyWorkThread, just classes derived from it. (In fact, our use of the MustInherit keyword means that we can never create an instance of a MyWorkThread object.)

Public MustInherit Class MyWorkThread

' create somewhere to hold the thread details...
Private m_Pause As New ManualResetEvent(True)
Private m_IsCancelled As Boolean
Private m_Control As TextBox


To get and set the text box, we need a property called Control:

    ' create a common property for setting our reporting control...
Public Property Control() As TextBox
Get
Return m_control
End Get
Set
m_control = value
End Set
End Property


To make life easier for the threads, we create another method that lets us change just the text on the boxes. So that we can see what's going on, we prefix the value we get with the name of the class. That way, we can see at a glance what class set the message.

    ' create a common property for updating our control...
Public Property ControlText() As String
Get
Return m_Control.text
End Get
Set
m_control.Text = Me.ToString & ": " & value
End Set
End Property


Finally, we need a method that will return a reference to the main application form. The Parent member of TextBox will return a WinForm object to us, but a WinForm object is no use to us if we want to get hold of properties and methods that we define on Form1. What we need to do is cast the WinForm object to a Form1 object, which we do using CType:

    ' create a property that will return the parent of the text box
' control, cast to a "Form1" type...
Public ReadOnly Property MainForm() As Form1
Get
Return CType(Control.Parent, Form1)
End Get
End Property


Creating a thread in VB.NET is really easy. All you have to do is instantiate a class and identify a method on that class as the entry point for the thread. We're going to assume that on WordCounter and CharCounter, this method will always be called StartThreading. Add this method to the MyWorkThread class definition, making sure you include the MustOverride directive. This forces anyone deriving from MyWorkThread to include his or her own definition of StartThreading. (C++ developers: this is a pure virtual function.)

    ' create a common mustoverride member to handle thread startup...


To start the thread, we're going to call the Go method. This creates a new ThreadStart object, then creates a new Thread object and then starts the thread:

    ' create something that will startup the thread...
Public Sub Go()

' now, initialize and kick off the thread...

End Sub


The AddressOf keyword returns a delegate to the StartThreading method in the class. (C++ developers: this is a function pointer.) For some reason, you can't Dim ThreadStart and provide a value in one line - it throws a weird error. It must be done on two separate lines.

When the thread is finished, we're going to assume that it calls the Finish method. All this does is set the text box to read Finished, but we could do something cooler in here if we wanted.

     ' Finish - a method that's called when the thread stops processing...
Public Sub Finish()
ControlText = "Finished!"
End Sub


#### Events

One curious thing about threading is that an "event" is not the same as a VB event, such as the OnClick method of a button WinForm control. This is because threading has its roots in traditional Win32 programming, which did not have events. An event in a threading context identifies something that happens to release a lock on an object. So, if your thread is blocking on a lock waiting for it to become free, when that lock does become free an "event" will be thrown and you'll stop blocking and be able to go into the locked code to do your processing.

The System.Threading namespace contains an object called ManualRaiseEvent. This is an object that exists to do nothing but provide -a block that can be in one of two states - signalled and non-signalled. If the block is non-signalled, it's "blocked", i.e. you can't go through it. If it's signalled, you can.

(For C++ developers, this is equivalent to the Win32 API CreateEvent call.)

We're going to use a ManualRaiseEvent to control our pause/resume functionality. Our thread is going to go around in an infinite loop. At the top of the loop it's going to ask the owner form for a copy of the text in the text box. ("Copy" is an important word here, and we'll discuss this in a moment.) It will then do its work, report its results and then "pause" itself. Whenever the text in the top box changes, all of the threads will be asked to "resume" themselves, and the cycle repeats. We can also ask the thread to cancel, in which case it will drop out of the infinite loop and come to a natural finish.

To pause the thread, we need to "reset" the ManualRaiseEvent object. This will put it into a non-signaled state:

    ' Pause - tell a thread to pause...
m_pause.Reset()
End Sub


To resume the thread, we need to "set" the object. This will put it into a signaled state:

    ' Resume - tell a thread to resume processing...
m_pause.Set()
End Sub


If we want to cancel the thread, we have to set the cancelled flag to true, and resume the thread:

    ' Cancel - stop the thread...
Public Sub Cancel()

' flag us as cancelled...
m_iscancelled = True

End Sub


It's worth noting that this isn't a "stop now or else" style command. This is a "can you finish up what you were doing and finish gracefully" style command. Ideally, when writing multithreaded code, this is the approach you want - get everything to finish naturally, rather than forcing it.

We'll include this, as it might be useful to know if we've been cancelled:

    ' IsCancelled - are we cancelled?
Public Function IsCancelled() As Boolean
Return m_IsCancelled
End Function


The last major method we need on this object is IsPaused. This is a bit of a misnomer, because if the thread is paused, it won't return - effectively it is the blocking function. What we will do, though, is return the opposite of IsCancelled out the other side. This will tell us if we should "keep working" or "quit". What happens when it's used is that the thread will come along say "am I paused". If it is paused, this function won't return. The thread will drop into a wait state at this point until a time when it's no longer paused. When it's not paused, it returns whatever IsCancelled is set to. So, if the thread has been cancelled, IsPaused will return False. If the thread hasn't been cancelled, it will return True.

    ' IsPaused - tells a thread to continue...
Public Function IsPaused() As Boolean

' wait on the pause object...
m_pause.WaitOne()

' now, return the opposite of cancelled, i.e.
' true means "keep going", false means "stop".
Return Not IsCancelled()

End Function


The WaitOne method is a member of ManualResetEvent. It tells Windows to block the thread until the object is signaled. (C++ developers: this is WaitForSingleObject.)

Finally, we want one final method that waits for the thread itself to finish. The Join method on Thread will wait until the thread has neatly finished its work:

    ' WaitUntilFinished - keeps waiting until the thread is ready to close...
Public Sub WaitUntilFinished()
End Sub


To call the threads, we need to wire in code behind the cmdStartThreads button. Before we do this, though, we need to define CharCounter and WordCounter, otherwise VS.NET will keep throwing errors while we're writing the code.

#### Building "CharCounter"

First of all in CharCounter, we set the text of our control to Started. Notice how we've inherited from MyWorkThread, and how we've provided a definition for the compulsory StartThreading member:

Imports System
Imports System.WinForms

Public Class CharCounter

' tell it we've started....
controltext = "Started!"


Now we can drop into our infinite loop. We use IsPaused to wait for a signal indicating that we need to process the results, or quit the loop:

        ' go into a loop and wait until we're asked to start processing...
Do While True

' are we paused?
If IsPaused() = False Then
Exit Do
End If


Next, we do the work and update our results. (We'll see the WorkText property in a moment.)

            ' get hold of the text...
Dim worktext As String = MainForm.WorkText
If worktext.Length = 1 Then
controltext = "1 char"
Else
controltext = worktext.Length.ToString & " chars"
End If


After we've done our work, we pause ourselves. The loop will flip back to the beginning and start blocking on IsPaused again, until the next time we are resumed.

            ' put the thread back into a wait state...

Loop


Finally, if we have finished, we call Finish:

        ' tell it we've finished...
Finish()

End Sub

End Class


#### Building "WordCounter"

WordCounter is virtually identical to CharCounter:

Imports System
Imports System.WinForms

Public Class WordCounter

' tell it we've started....
controltext = "Started!"

' go into a loop and wait until we're asked to start processing...
Do While True

' are we paused?
If IsPaused() = False Then
Exit Do
End If

' get hold of the text...
Dim worktext As String = MainForm.WorkText

' split the text into words...
Dim words() As String = worktext.split(" ".tochararray)

' report the number of words...
If words.Length = 1 Then
controltext = "1 word"
Else
controltext = words.Length & " words"
End If

' put the thread back into a wait state...

Loop

' tell it we've finished...
Finish()

End Sub

End Class


Now we can tweak Form1 and get it to actually create our threads. The first thing we need is a property to get and set the text from the text box on the form that contains the text we want to process. This is where our thread synchronization comes in. Whenever we hear a TextChanged event on this text box, we want to set the value for the WorkText property, but only if no one is reading it. (If threads are reading it, we need to wait until they've all finished, then we go in and write it.) Likewise, when any of our threads start, they're going to want to get the value for the property, but only if no one is writing it. Therefore, we need to create a reader/writer lock that protects this property from any synchronization problems.

First off, create these member variables at the top of Form1:

    ' create somewhere to keep track of all the threads...
Const NUMTHREADS As Integer = 1

' create somewhere to keep hold of the text...
Dim m_WorkText As String


A System.Threading.ReaderWriterLock creates the lock that we've been talking about. To use it, we build a property that looks like this:

    ' create a property that returns the text to process... we want to
Public Property WorkText() As String
Get

' lock the object for reading...

' get the value back, as we're sure no one's writing it...
WorkText = m_WorkText

' release the lock on the object...

End Get
Set

' lock the object for writing...
m_WorkTextLock.AcquireWriterLock(-1)

' set the value, as we're sure no one's reading it
' and no one else is writing it...
m_WorkText = Value

' release the lock...
m_WorkTextLock.ReleaseWriterLock()

End Set
End Property


The -1 that we specify to AcquireReaderLock and AcquireWriterLock tells the object that we want to wait "forever" for the lock to become available. This is a fairly typical thing to find in multithreaded code and can cause problems if not done with respect. It's of paramount importance that you properly unlock blocks of code once you've finished using them. For example, if we forget to release a "write" lock, no other locks can ever been opened and everything will grind to a halt. You also have to have the same number of "lock" and "unlock" commands, e.g. if you lock something five times, you have to unlock it five times.

What's smart about this is that it's transparent to the calling thread - i.e. we've taken all the complexity of synchronizing access to the data and isolated the problem and solution from the thing that needs to use it. Each of our 16 threads will call Form1.WorkText to find out what text it's supposed to be processing. If it's not allowed to have access to the m_WorkText data because it's in the process of being changed, this call will automatically block until the owner of the "write" lock has finished.

To create the threads, we need a function called StartThreads. This will firstly loop through all the threads that we know about, creating either a WordCounter or CharCounter thread, depending on the thread number:

    Sub StartThreads()

Dim n As Integer
For n = 0 To NUMTHREADS - 1

Select Case n Mod 2
Case 0
Case Else
End Select


Once we've created the thread, we tell the object which text box we want it to report its results in and start things running:


' configure the thread and start it...

Next

End Sub


    Protected Sub cmdStartThreads_Click(ByVal sender As Object, _
ByVal e As System.EventArgs)
End Sub


To make the threads respond to a change, we need to respond to the TextChanged event. Firstly, we copy the value in the text box to our m_WorkText member variable using the WorkText property. This ensures that the locking is done properly, i.e. the value cannot be set if any of the other threads are reading it.

    Public Sub txtText_TextChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles txtText.TextChanged

' update our value...
WorkText = txtText.Text



Once we've set the property, we loop through all of our threads and tell them to resume:

        ' now, resume all of our threads...
Dim n As Integer
For n = 0 To NUMTHREADS - 1
Next

End Sub


That's it! Now if you click StartThreads and change the text in the text box, the threads will process the text and return their results.

Stopping the threads is the same deal as creating them:

    Sub StopThreads()

' loop through the threads stopping them as we go...
Dim n As Integer
For n = 0 To NUMTHREADS - 1

' do we have a thread?
If Not m_threads(n) Is Nothing Then

' tell the thread to cancel...

' now, wait for the thread to stop...

End If

Next

End Sub


This time, however, we call Cancel to tell the thread to finish working. It will stop blocking on IsPaused, quit the loop and get to the end of StartThreading. When StartThreading returns, the thread is considered to be "dead". WaitUntilFinished will call Join on the m_Thread object and will block until the thread dies, i.e. we return from StartThreading.

For neatness, we need to call StopThreads when the application itself closes:

    Public Sub ThreadForm_Closing(ByVal sender As Object, _
ByVal e As System.ComponentModel.CancelEventArgs) Handles Form1.Closing
End Sub


### Conclusion

In this article we saw how threads can be used in .NET courtesy of the System.Threading namespace. We also saw a practical example of synchronization in use, and how multiple threads can be used to work on the same piece of data. Finally, remember that although this article has been focused on Visual Basic, the same techniques work exactly the same in C#.

            [download relative material]

0 0