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#.
What is a thread?
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.
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!
What we need is something called a "reader/writer lock". This kind of lock needs a little more information from you, but can reduce the effect of bottlenecks. Imagine you had two threads both trying to read from m_MyVariable at the same time. Just like when reading files from disk we can open the file to read as many times as we like - i.e. we want synchronous access. What we do is ask each thread to open the lock for "reading".
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!
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.
The point we're trying to get to is this: when the user clicks the Start Threads button, we want to create 16 threads. (There's no reason for choosing 16 threads - we just want to create a few threads to create a fun example. We could do this with 2 or 200 threads, although it's worth noting like a lot of Windows development, there's a point where adding more threads doesn't improve our application. There's no hard and fast rule about "how many threads is the right number of threads", so if you do choose to use them, feel your way around the problem to get the best results.) Eight of these threads will be handled by a class called WordCounter and will be responsible for counting the words in the text. The other will be called CharCounter and will be responsible for counting the number of characters. When the user clicks Stop Threads, or closes the application, we want to gracefully close all of the threads.
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 Dim m_ThreadOutputs(NUMTHREADS) As TextBox
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' of the StartThreading button...Dim margin As Integermargin = cmdStartThreads.Bounds.Top - txtText.Bounds.Bottom
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... m_threadoutputs(n) = New TextBox() m_threadoutputs(n).Text = "Thread #" & n.ToString
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 m_threadoutputs(n).location = New System.Drawing.Point(midpoint, y) End If m_threadoutputs(n).Size = New System.Drawing.Size(controlwidth, controlheight) m_threadoutputs(n).Visible = True
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... Me.Controls.Add(m_threadoutputs(n))
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 NextEnd 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_Thread - an instance of a System.Threading.Thread object that lets us control the thread,
- 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.
Create a new class called MyWorkThread and add these namespaces:
Imports System Imports System.Threading 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_Thread As ThreadPrivate m_Pause As New ManualResetEvent(True)Private m_IsCancelled As BooleanPrivate 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
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... Public MustOverride Sub StartThreading()
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() ' create a threadstart to fire the thread... Dim start As ThreadStart start = New ThreadStart(AddressOf Me.StartThreading) ' now, initialize and kick off the thread... m_thread = New Thread(start) m_thread.Start() 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
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... Public Sub PauseThread() 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... Public Sub ResumeThread() 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 ' resume the thread... ResumeThread() 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() m_thread.Join() End Sub
Calling the threads
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.
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 Imports System.Threading Public Class CharCounter Inherits MyWorkThread Public Overrides Sub StartThreading() ' 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... PauseThread() Loop
Finally, if we have finished, we call Finish:
' tell it we've finished... Finish() End Sub End Class
WordCounter is virtually identical to CharCounter:
Imports System Imports System.WinForms Imports System.Threading Public Class WordCounter Inherits MyWorkThread Public Overrides Sub StartThreading() ' 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... PauseThread() Loop ' tell it we've finished... Finish() End Sub End Class
Supporting the threads
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 Dim m_ThreadOutputs(NUMTHREADS) As TextBox Dim m_Threads(NUMTHREADS) As MyWorkThread ' create somewhere to keep hold of the text... Dim m_WorkText As String Dim m_WorkTextLock As New ReaderWriterLock()
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 ' handle reader/writer locks properly... Public Property WorkText() As String Get ' lock the object for reading... m_WorkTextLock.AcquireReaderLock(-1) ' get the value back, as we're sure no one's writing it... WorkText = m_WorkText ' release the lock on the object... m_WorkTextLock.ReleaseReaderLock() 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() ' create new threads... Dim n As Integer For n = 0 To NUMTHREADS - 1 ' create a thread... Select Case n Mod 2 Case 0 m_threads(n) = New WordCounter() Case Else m_threads(n) = New CharCounter() 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... m_threads(n).Control = m_ThreadOutputs(n) m_threads(n).Go() Next End Sub
Of course, we need to wire StartThreads into the cmdStartThreads button:
Protected Sub cmdStartThreads_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) StartThreads() 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 If Not m_threads(n) Is Nothing Then m_threads(n).ResumeThread() 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.
Closing the threads
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... m_threads(n).Cancel() ' now, wait for the thread to stop... m_threads(n).WaitUntilFinished() ' finally, remove the thread... m_threads(n) = Nothing 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 StopThreads() End Sub
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#.