Using Threading to Build a Responsive Application with Visual Studio 2005

 

Brad McCabe

June 2005

Applies to:
   Visual Basic 2005
   Visual Studio 2005

Summary: Building multithreaded applications is easy with the new BackgroundWorker component in Visual Studio 2005, and it allows you to create interactive applications with minimal work. (11 printed pages)

Download the associated VS05ThreadingSampleCode.msi code sample.

Contents

Introduction
Long-Running Task Example
Multithreading Example Using BackgroundWorker
Conclusion

Introduction

One of the most powerful features introduced with Visual Basic .NET was the ability to create multithreaded applications. Many Visual Basic developers did not take advantage of this newfound power because of the complex nature and challenges for developing a multithreaded application.

Before we look at how this is easier to do in Visual Basic 2005 let's first look at a common developer challenge: long-running tasks which often restrict user input or interaction with the system during there execution.

Long-Running Task Example

For this example, our long-running task will be the calculation of the Fibonacci number for a given integer. I understand that this is not something most developers do in their applications, but it is a good example that anyone can run without the need for a database or other common infrastructure. The types of long-running tasks you might want to address in your applications are expensive database operations, calls to legacy systems, or calls to external services or other resource-intensive operations.

To create this project, start by creating a new Windows Forms application with a progress bar, two buttons, a numeric input box, and a label for the results. You can name your buttons startSyncButton and cancelSyncButton, and set the text of the label to (no result). After carefully laying these out, the form should look something like this:

ms379602.threadinginvb2005_01(en-US,VS.80).gif

Figure 1. Creating a new Windows Forms application

To this form we will add the code to compute the Fibonacci number:

    Function ComputeFibonacci(ByVal n As Integer) As Long
        ' The parameter n must be >= 0 and <= 91.
        ' Fib(n), with n > 91, overflows a long.
        If n < 0 OrElse n > 91 Then
            Throw New ArgumentException( _
                "value must be >= 0 and <= 91", "n")
        End If

        Dim result As Long = 0

        If n < 2 Then
            result = 1
        Else
            result = ComputeFibonacci(n - 1) + _
            ComputeFibonacci(n - 2)
        End If

        ' Report progress as a percentage of the total task.
        Dim percentComplete As Integer = CSng(n) / _
        CSng(numberToCompute) * 100

        If percentComplete > highestPercentageReached Then
            highestPercentageReached = percentComplete
            Me.ProgressBar1.Value = percentComplete
        End If

        Return result
    End Function

This code is fairly straightforward. It will compute the value by calling itself recursively. While this will execute fast for small numbers, as your input numbers increase you will see that performance decays rapidly.

On each pass through the code the function will update a progress bar on the screen to keep the user aware of the progress and advised that the application is still running.

Now we will put a little code behind the start button to run our function.

    Private Sub startSyncButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles startSyncButton.Click
        ' Reset the text in the result label.
        result.Text = [String].Empty

        ' Disable the UpDown control until 
        ' the synchronous operation is done.
        Me.numericUpDown1.Enabled = False

        ' Disable the Start button until 
        ' the synchronous operation is done.
        Me.startSyncButton.Enabled = False

        ' Enable the Cancel button while 
        ' the synchronous operation runs.
        Me.cancelSyncButton.Enabled = True

        ' Get the value from the UpDown control and store it
  ' in the globle variable numberToCompute.
        numberToCompute = CInt(numericUpDown1.Value)

        ' Reset the variable for percentage tracking.
        highestPercentageReached = 0

        ' Start the synchronous operation.
        result.Text = ComputeFibonacci(numberToCompute).ToString

        ' Enable the UpDown control.
        Me.numericUpDown1.Enabled = True

        ' Enable the Start button.
        startSyncButton.Enabled = True

        ' Disable the Cancel button.
        cancelSyncButton.Enabled = False
    End Sub

There is nothing out of the normal, here; this is how countless applications in the world are designed. The user clicks the button and we compute the Fibonacci value and display it on the screen; however, this design has one major draw back.

When the button is pressed, the main thread, which is also responsible for handling request from the UI, is busy computing the Fibonacci value. If you start this application and enter a larger number, such as 50, you will see the problems your users will experience. After clicking the Start button, attempt to minimize the application or move the window around. While running, the application is unresponsive to any additional user information or responds very sluggishly.

ms379602.threadinginvb2005_02(en-US,VS.80).gif

Figure 2. Even though the function is running, the application appears to be unresponsive.

In addition to being sluggish and unresponsive, there is no way to allow the user to cancel the process. What option do users have if they enter a large number by mistake or don't wish to wait any longer?

To demonstrate this, put the following sample code behind the Cancel button:

    Private Sub cancelSyncButton_Click(ByVal sender As System.Object,_
 ByVal e As System.EventArgs) Handles cancelSyncButton.Click
        MsgBox("Cancel")
    End Sub

This is very basic code that will display a message box to show that we have requested a cancel. If you run the application and enter another larger number you will notice that clicking the Cancel button does not do anything while the process is running.

The best way to resolve this is to move our long-running process over to a different thread. This will leave our main thread open to respond to user input and keep our application responsive to the user.

Multithreading Example Using BackgroundWorker

In Visual Basic 6.0 it was not possible to create a new thread without a series of hacks involving timers to get around the problem. With Visual Basic .NET, it was as easy as creating a new thread object, passing a method you want to run, and calling the start method on the thread object.

        Dim myThread As New Thread(AddressOf MyFunction)
        myThread.Start()

However, with great power comes great responsibility. While Visual Basic 6.0 made creating and starting new threads easy, care had to be taken to design applications in the proper manner or risk numerous bugs and problems.

Because of the complexity of proper design, multithreading was not widely adapted by large numbers of Visual Basic developers. With Visual Basic 2005, the process is much easier and safer with the new BackgroundWorker component.

To demonstrate how simple it is to make the application multithreaded and more responsive to the user, let's create a new form called MultiThreaded with the same code and layout as our original form. This time, however, you should name the buttons startAsyncButton and cancelAsyncButton. The reason is that this time we will be executing our code asynchronously and not blocking the main thread.

The first thing we are going to do to our new form is to open it in design mode and drag-and-drop a BackgroundWorker component from under the Components section of the Toolbox onto the form. You will want to set the WorkerReportProgress and WorkerSupportsCancellation properties to true in the Properties window for the component. These settings will enable us to update the progress bar and stop processing, as you will see later.

ms379602.threadinginvb2005_03(en-US,VS.80).gif

Figure 3. The new BackgroundWorker component makes creating multithreaded applications much simpler.

After completing this, we will change our Start button code to the following:

    Private Sub startAsyncButton_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) _
    Handles startAsyncButton.Click

        ' Reset the text in the result label.
        result.Text = [String].Empty

        ' Disable the UpDown control until 
        ' the asynchronous operation is done.
        Me.numericUpDown1.Enabled = False

        ' Disable the Start button until 
        ' the asynchronous operation is done.
        Me.startAsyncButton.Enabled = False

        ' Enable the Cancel button while 
        ' the asynchronous operation runs.
        Me.cancelAsyncButton.Enabled = True

        ' Get the value from the UpDown control.
        numberToCompute = CInt(numericUpDown1.Value)

        ' Reset the variable for percentage tracking.
        highestPercentageReached = 0

        ' Start the asynchronous operation.
        backgroundWorker1.RunWorkerAsync(numberToCompute)
    End Sub

You will notice that we are going to call the RunWorkerAsync method on the BackgroundWorker component, and that we remove all of the code after we call this method.

When you are ready to invoke code on a separate thread you call the RunWorkerAsync method, this will generate an event on the BackgroundWorker called DoWork. It is in this event that we will compute the Fibonacci value.

    ' This event handler is where the actual work is done.
    Private Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork

        ' Get the BackgroundWorker object that raised this event.
        Dim worker As System.ComponentModel.BackgroundWorker= _
            CType(sender, System.ComponentModel.BackgroundWorker)

        ' Assign the result of the computation
        ' to the Result property of the DoWorkEventArgs
        ' object. This is will be available to the 
        ' RunWorkerCompleted eventhandler.
        e.Result = ComputeFibonacci(e.Argument, worker, e)
    End Sub

We could put our code directly in here to process our asynchronous event but it is a better practice to keep that in a separate procedure for most situations. In our example we will use the existing ComputeFibonacci function but with a few small changes.

    Function ComputeFibonacci( _
        ByVal n As Integer, _
        ByVal worker As System.ComponentModel.BackgroundWorker, _
        ByVal e As System.ComponentModel.DoWorkEventArgs) As Long

        ' The parameter n must be >= 0 and <= 91.
        ' Fib(n), with n > 91, overflows a long.
        If n < 0 OrElse n > 91 Then
            Throw New ArgumentException( _
                "value must be >= 0 and <= 91", "n")
        End If

        Dim result As Long = 0

        ' Abort the operation if the user has canceled.
        ' Note that a call to CancelAsync may have set 
        ' CancellationPending to true just after the
        ' last invocation of this method exits, so this 
        ' code will not have the opportunity to set the 
        ' DoWorkEventArgs.Cancel flag to true. This means
        ' that RunWorkerCompletedEventArgs.Cancelled will
        ' not be set to true in your RunWorkerCompleted
        ' event handler. This is a race condition.
        If worker.CancellationPending Then
            e.Cancel = True
        Else
            If n < 2 Then
                result = 1
            Else
                result = ComputeFibonacci(n - 1, worker, e) + _
                         ComputeFibonacci(n - 2, worker, e)
            End If

            ' Report progress as a percentage of the total task.
            Dim percentComplete As Integer = _
                CSng(n) / CSng(numberToCompute) * 100
            If percentComplete > highestPercentageReached Then
                highestPercentageReached = percentComplete
                worker.ReportProgress(percentComplete)
            End If
        End If

        Return result

    End Function

One of the most common errors that people commit with Visual Basic when doing multithreaded applications is to attempt to access components and objects created on a different thread, such as the main UI thread. This can create unpredictable and erratic behavior in your application because not every object is thread-safe.

The first thing you will notice is that we are going to pass the BackgroundWorker and the event arguments to the compute function. This will allow us to raise events back up the main thread to update controls such as the progress bar instead of doing this from the working process.

After each pass through the ComputeFibonacci function we call the ReportProgress method on the BackgroundWorker component. When we do this the components ProgressChanged event will fire. This event is raised back to the main UI thread to update the ProgressBar. To avoid any cross-thread expectations, we do not update the ProgressBar on our new thread.

The code for this event is pretty simple:

    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, ByVal e As ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged

        Me.progressBar1.Value = e.ProgressPercentage

    End Sub

If we compile and run our new form the first thing you notice is that if you enter a large number, such as 50, you are able to minimize, maximize, or move the form with no problems or delays like before. This is because the main UI thread is available to process this request while the secondary thread processes our Fibonacci value.

Since the main thread is free to respond to user input, we can put a few lines of code behind the Cancel button to give the user the ability to abort the process.

    Private Sub cancelAsyncButton_Click( _
    ByVal sender As System.Object, _
    ByVal e As System.EventArgs) _
    Handles cancelAsyncButton.Click

        ' Cancel the asynchronous operation.
        Me.backgroundWorker1.CancelAsync()

        ' Disable the Cancel button.
        cancelAsyncButton.Enabled = False

    End Sub

Notice that to abort the background process we simple call the CancelAsync method of the BackgroundWorker. This will set the CancellationPending property to true on the component, which we check on each pass through our ComputeFibonacci function.

After the function has completed its work, the BackgroundWorker will raise the RunWorkerCompleted event. In this event we have placed all of the code to display the results to the user, reset the buttons on the screen, and prepare for the next operation.

    Private Sub backgroundWorker1_RunWorkerCompleted( _
    ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) _
    Handles backgroundWorker1.RunWorkerCompleted

        ' First, handle the case where an exception was thrown.
        If Not (e.Error Is Nothing) Then
            MessageBox.Show(e.Error.Message)
        ElseIf e.Cancelled Then
            ' Next, handle the case where the user canceled the 
            ' operation.
            result.Text = "Canceled"
        Else
            ' Finally, handle the case where the operation succeeded.
            result.Text = e.Result.ToString()
        End If

        ' Enable the UpDown control.
        Me.numericUpDown1.Enabled = True

        ' Enable the Start button.
        startAsyncButton.Enabled = True

        ' Disable the Cancel button.
        cancelAsyncButton.Enabled = False
    End Sub

Conclusion

I hope that the examples above have shown you how it is much easier to work with multithreaded applications in Visual Basic 2005 using the BackgroundWorker component and its properties, methods, and events. With just a few modifications we were able to update our code to create a more interactive Windows Forms application while enabling our users to cancel or abort a task. In order to provide your users with the best possible performance and interactive applications, you should look to use a multithreaded design where it makes sense to offload extensive or long-running tasks from your main UI thread.

© Microsoft Corporation. All rights reserved.