Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual Basic 2005 Express Edition - From Novice To Professional (2006)

.pdf
Скачиваний:
387
Добавлен:
17.08.2013
Размер:
21.25 Mб
Скачать

490

C H A P T E R 1 8 T H R E A D I N G

Next, let’s add the code to the CalcButton's Click event to start the BackgroundWorker thread. Add the highlighted lines to the event handler:

Private Sub calcButton_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles calcButton.Click

resultsBox.Items.Clear()

startingNum = Long.Parse(startingNumber.Text)

BackgroundWorker1.WorkerReportsProgress = True BackgroundWorker1.RunWorkerAsync()

End Sub

Calling RunWorkerAsync() on the BackgroundWorker control starts the code in the DoWork event handler running on a separate thread. So, the code behind the calcButton just empties the list box, puts our starting number into a member variable, and then starts up the new thread.

All that remains is to fix the DoWork event handler so that it doesn’t directly try to update the list box with the results, but instead fires the ReportProgress() method.

First, go back to the form editor. Click on the BackgroundWorker control and then look at the list of events that it supports in the Properties window. You can see these in Figure 18-6.

Figure 18-6. The events supported by the BackgroundWorker control

C H A P T E R 1 8 T H R E A D I N G

491

Double-click on the ProgressChanged event in the event list to drop back into the source editor for the BackgroundWorker’s ProgressChanged event handler.

Let’s add the code in here to update our list box:

Private Sub BackgroundWorker1_ProgressChanged( _

ByVal sender As System.Object, _

ByVal e As System.ComponentModel.ProgressChangedEventArgs) _

Handles BackgroundWorker1.ProgressChanged

resultsBox.Items.Add(e.UserState)

End Sub

Like many events, the ProgressChanged event gets a set of arguments passed to it as the second parameter, e. In the case of ProgressChangedEventArgs, there is a member called UserState. This is an object that can be passed to the event handler when the code calls ReportProgress(). In our case, we’re just going to pass a string across so all the code here needs to do is add that string straight into the list box.

Go back up to the DoWork event handler and change the last line of code so that it calls ReportProgress() instead of trying to update the list box:

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _

Handles BackgroundWorker1.DoWork

For currentNum As

Long = startingNum To startingNum + 999

Dim factorial

As

Long = 0

For i As Long

= 1 To currentNum

factorial +=

i

Next

 

 

BackgroundWorker1.ReportProgress( _

0, String.Format("The factorial of {0} is {1}", _

currentNum, factorial))

Next

End Sub

ReportProgress() actually takes two parameters. The first is typically the percentage done. Remember, ReportProgress() and ProgressChanged were originally envisaged to provide you with a way to update a progress bar on the form, so it makes sense that the first parameter is the actual percentage complete. We won’t bother with this, so our code just passes across 0 as the first parameter. The second parameter is the object that will eventually end up as ProgressChangedEventArgs.UserData, which in our case is a string.

492

C H A P T E R 1 8 T H R E A D I N G

Phew—that was a lot of typing. The end result, though, is really not all that different from the original code we had. Instead of our factorial calculating code being held in a single method, we now have it split across three.

The first part is fired when the user clicks the Calculate button. This grabs the number to start with and stores it in a member variable, and then starts the BackgroundWorker running. The second is the BackgroundWorker DoEvents handler, which calculates our factorials and holds the bulk of the code that was originally in the button’s click handler. The third piece of code just updates the user interface of our application in a ProgressChanged event handler.

Run the program now. Notice how this time the user interface always redraws itself nicely and you’re free to click around the list, selecting items and browsing, while the program goes about its business of calculating factorials.

Don’t throw this program away just yet—we’ll be “finessing” it a little more in the next section.

The biggest problem I find when working with threads is the guy sitting there using the program. You could write a wonderfully elegant multithreaded solution to keep everything snappy and responsive during a long-running operation, and then you get a phone call from an irate user asking why you didn’t give him a way to cancel something. Users have no appreciation for what goes on behind the scenes.

The fact is, any time you are really going to want to use a thread, there’s a good chance some user somewhere will want to cancel whatever it is your thread is doing. Perhaps it’s taking too long for them (they have short attention spans). Perhaps they hit the wrong button and love your code, but really don’t have the time to sit and wait for the first five million prime numbers to get calculated.

The BackgroundWorker control provides tools to let you cope with these annoyances. Earlier when you saw the BackgroundWorker control’s event list, you may have noticed

that it supports three events. DoWork and ProgressChanged we used. The third event, RunWorkerCompleted, we didn’t. The RunWorkerCompleted event is fired when the code in DoWork completes, either by natural causes or because it was cancelled. Working hand in hand with this are three properties: CancellationPending, IsBusy, and

WorkerSupportsCancellation.

Setting the WorkerSupportsCancellation property to True enables you to cancel whatever the control is doing with a quick call to CancelAsync(). At any time you can check to see whether the control is still doing its thing with the IsBusy property, and see whether it is waiting to cancel by checking the CancellationPending property. Let’s extend the earlier example to see all these things in action, and provide our users with a way to cancel the thread if they so desire.

C H A P T E R 1 8 T H R E A D I N G

493

Try It Out: Cancelling a Thread

Go back to the program you were just working on and take a look at the form editor once again. Click on the BackgroundWorker control and use the Properties window to set the SupportsCancellation property to True, just as in Figure 18-7.

Figure 18-7. Setting the SupportsCancellation property to True lets us write code to cancel background threads.

Now bring up the event list of the control and double-click the RunWorkerCompleted event to get its handler stub into the code window. We won’t write any code into the event just yet, but it’s handy to have the event stub there in the code window so we don’t go jumping around between designer and source editor throughout the example.

Now, go back up to the calcButton_click event. Let’s add some code in there to change the text in the button to Cancel, and to enable us to actually cancel the worker if we need to. Make the following highlighted changes to the code:

Private Sub calcButton_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles calcButton.Click

If BackgroundWorker1.IsBusy Then

BackgroundWorker1.CancelAsync()

494

C H A P T E R 1 8 T H R E A D I N G

Else

resultsBox.Items.Clear()

startingNum = Long.Parse(startingNumber.Text)

BackgroundWorker1.WorkerReportsProgress = True BackgroundWorker1.RunWorkerAsync() calcButton.Text = "Cancel"

End If

End Sub

When the button is clicked now, it first checks to see whether the thread is running. If it is, the code calls CancelAsync() to cancel it. Otherwise, the same code as before runs, but with an additional line to change the text of the button to Cancel. This means that as soon as the user clicks the button, the user instantly gets visual feedback for how to stop the long-running process from doing what it does.

Simply telling the BackgroundWorker to cancel is not enough. Our code in the DoWork handler needs to respond to the cancellation request by dropping out:

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _

Handles BackgroundWorker1.DoWork

For currentNum As Long = startingNum To startingNum + 999

If BackgroundWorker1.CancellationPending Then

Return

End If

Dim factorial As Long = 0

For i As Long = 1 To currentNum factorial += i

Next BackgroundWorker1.ReportProgress( _

0, String.Format("The factorial of {0} is {1}", _ currentNum, factorial))

Next

End Sub

The first thing the loop does now is check whether a cancel request is pending. If it is, the code just returns, breaking out of the DoWork handler.

C H A P T E R 1 8 T H R E A D I N G

495

All that remains is to code up the RunWorkerCompleted() method, to put the text on the calcButton back the way it was:

Private Sub BackgroundWorker1_RunWorkerCompleted( _

ByVal sender As System.Object, _

ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _

Handles BackgroundWorker1.RunWorkerCompleted

calcButton.Text = "Calculate"

End Sub

That’s all there is to it. Run the program now to see it all in action.

Just to summarize what we did here, we enabled thread cancelling by setting the WorkerSupportsCancellation property to True on the control. Then to cancel the event, the code just calls CancelAsync() on the control, which in turn flips the CancellationPending flag on. Our code in DoWork just needs to check this flag to see whether it needs to exit early.

Race Conditions and Deadlocks

Computer programs are complex enough beasts when they are just doing one thing at a time. When you start to explore the world of threads, you end up with programs that do more than one thing at once, and life can get very complicated.

There are two specific conditions that you need to be aware of when you start working with multithreaded programs: race conditions and deadlocks.

A race condition occurs when two threads “race” for access to a resource. For example, you may have an object that’s used in two threads. If one thread tries to change a value in the object while another tries to do the same thing, a race condition can occur. You can’t guarantee which thread will get to the object first, and thus your code could end up with some nasty bugs.

To prevent this kind of thing from happening,Visual Basic has a special keyword called SyncLock. SyncLock is used to “lock” an object so that only the thread that locked it can work with it. For example:

SyncLock myEmployee myEmployee.IncreaseSalary( 10 )

End SyncLock

This locks the myEmployee object in order to increase its salary, preventing any other threads from also working with the object (perhaps to calculate the employee’s bonus or tax) until the lock block has finished.

496

C H A P T E R 1 8 T H R E A D I N G

WHY WE DIDN’T USE A LOCK EARLIER

The example in the preceding “Try It Out” uses a shared variable, startingNum, to figure out which numbers we want to calculate factorials for. Now startingNum was initialized on the program’s main thread, but read and worked with on the BackgroundWorker thread. It figures, then, that it should have been locked, but it wasn’t. Why?

There are two reasons. First, locks can be used only with reference types (objects), and not value types (Integer, Boolean, Double, and so on). So a lock wouldn’t have worked there because startingNum is a Long and not a reference type. Second, the order of things in our sample program guarantees that there won’t be a locking problem. The value is set into the variable before the second thread is started. Had the value been changed while the thread was running, that could have caused all sorts of problems.

In general, whenever you need to access an object from a thread other than the main one (that is, a thread you created), you should lock it.

The opposite of a race condition is a deadlock. Whereas race conditions can cause your programs to do very strange things, deadlocks can cause them to just hang and do nothing for no apparent reason.

A deadlock occurs when two threads lock each other’s resources. For example, one thread could lock an employee object and then wait for access to a department object. A second thread could have locked the department object and be waiting for the now locked employee object. The net result is that both threads stop dead in the water.

There is no magic keyword to prevent deadlocks from happening. You just need to be careful when writing your own code to make sure that you don’t end up with a situation where a deadlock can occur.

Personally, I find the best way to achieve this is to use threads very sparingly indeed. If you have a desperate and clear need for a thread (to update the UI during a long-running operation, or to wait for something to happen while the main code continues), use them. Don’t go nuts and drop threads all over the place, though, or you’ll quickly end up in a huge mess of a program that is unbelievably difficult to debug.

Summary

The BackgroundWorker control and the Timer control provide you with a great deal of power when it comes to writing programs that seem to do more than one thing at once. Behind the scenes, the BackgroundWorker uses a bunch of .NET method calls to manage all the threading that quite frankly is pretty scary to get to grips with. The coolest part of the BackgroundWorker control is that we don’t have to deal with that nasty stuff. Any time you want to fire up a new thread, just create an instance of the BackgroundWorker control (even in code), hook up the three events, and off you go.

C H A P T E R 1 9

■ ■ ■

Where to Now?

I can still clearly remember when I finished reading my first programming book. I had dutifully read all the pages, tried out all the examples, and done pretty much everything I had been told to do. Buoyed with my newfound knowledge, I sat down to write the next great killer application—and a strange thing happened. Well, more to the point, nothing happened. I didn’t know where to start. I had no idea what I should do to begin my program. The book had shown me all the parts of the puzzle and had shown me how some of the bigger pieces fit together, but I lacked the experience to finish the jigsaw totally on my own.

That’s where some of you are right now. Although we’ve certainly covered most of the cool features of Visual Basic 2005 Express, you may be wondering just where you go from here. When you put the book down, where can you turn for more help and advice? How can you start your first real program? Surely there’s still more to learn?

The answer to that last question is an unequivocal yes! Programming is all about learning, constantly. Not a coding day will go by without you learning something new. As old as it makes me feel to say it, after 23 years of programming I still learn new stuff every day.

Thankfully, though, the days of sitting in your den completely isolated from the rest of the world are over. There are plenty of resources out on the Internet, both official Microsoft ones and unofficial “fan” sites, to move you onward in your journey and to move you past the next hurdle: writing your first homegrown application.

Throughout this chapter then, I’ll point you in the right direction, starting with some cool pieces of code from Microsoft known as starter kits, and moving all the way through to the great sites and blogs that you should definitely keep an eye on to learn more. By the end of the chapter, you’ll definitely be in great shape to strike out on your own.

Starter Kits

The problem of not knowing exactly how to start writing your own programs is so common that Microsoft came up with a fantastic solution: starter kits. The idea behind starter kits is that “out of the box” you get a completely working program, documentation to help

497

498 C H A P T E R 1 9 W H E R E T O N O W ?

you understand what it does, and a set of code that you can then modify and tweak, effectively shaping it into the application you need.

Visual Basic 2005 Express ships with two starter kits: the Movie Collection starter kit and the Screen Saver starter kit. You can access them by simply starting a new project. The starter kits are shown at the end of the list of project templates, as shown in Figure 19-1.

Figure 19-1. You can access the starter kits just by clicking on Start New Project.

When you choose a starter kit—for example, the Movie Collection starter kit—what happens is that VB Express creates a new project with a bunch of code and other resources (forms, images, and so on) inside. It also displays a web page telling you what the starter kit does and how to modify it. If you start a new project based on the Movie Collection starter kit, for example, VB Express will look like Figure 19-2.

C H A P T E R 1 9 W H E R E T O N O W ?

499

Figure 19-2. Open the Movie Collection starter kit, and your development environment will look like this.

There are more starter kits available beyond the basic two that ship with VB Express. If you point your browser at the Microsoft Express home page (http://msdn.microsoft.com/ vstudio/express), you’ll see a list of Express products on the left side of the page, as in Figure 19-3.