Advanced Basics

Doing Async the Easy Way

Ken Getz

Code download available at:AdvancedBasics0503.exe(152 KB)

Contents

Single-Thread Conflicts
Using a Second Thread
Using Invoke to Avoid Thread Contention
Introducing BackgroundWorker
Canceling the Search

If you've been following Ted Pattison's excellent series of Basic Instincts columns on multithreading and asynchronous behavior, you should by now be an expert on handling the issues involved in working with multiple threads in Windows®-based apps. Recently, a friend, who evidently hadn't read Ted's columns, sent me a Windows-based application that performed a very lengthy file search operation that locked the user interface while executing. Users couldn't interact with, move, or resize the form while the main activity was running, so they weren't happy. He wanted to know what he could do to make the app more responsive. The answer, of course, is to perform the work in a background thread.

I recommended Ted's series of columns to my friend, but couldn't resist fixing things up on my own in the meantime. The application and how you can safely perform a background operation with as little code as possible—both in Visual Basic® .NET 2003 and in Visual Basic 2005—is this month's topic.

Single-Thread Conflicts

If you've downloaded the sample associated with this column, try running the Async2003 example in Visual Studio® .NET 2003. On the sample form, choose a drive that contains many files, enter some parameters other than *.* if you like, select the Search Subfolders option, and then click Simple Search. While the search is running, try interacting with the form, perhaps attempting to move or resize it. Try clicking Cancel Search. I dare you. You can't, right? Because the form is performing the search in the main thread, the user interface is blocked while the search is proceeding. Once the search has been completed, the listbox on the form should contain the full list of found files as shown in Figure 1.

Figure 1 File Search Application

Figure 1** File Search Application **

The simple search creates an instance of the FileSearch class (included in the sample project), sets some of its properties, adds an event handler for its FileFound event, and calls the FileSearch object's Execute method:

Dim fs As New FileSearch(Me.cboDrives.Text, Me.txtFileSpec.Text, _ Me.chkSearchSubfolders.Checked) AddHandler fs.FileFound, AddressOf EventHandler fs.Execute()

The FileSearch.Execute method sets up internal parameters and then calls the private FileSearch.Search method, which calls itself recursively looking for matches against the requested file specification. In Figure 2, the Globals.Cancelled property is a Shared Boolean value in the Globals class—clicking the Cancel button (if you ever get a chance) sets this value to True. Because forms in Windows-based applications are single-threaded, all the code called from the form runs in a single thread, and prevents user interface interactions while it's running.

Figure 2 Searching for Files

Private Sub Search(ByVal Path As String) Dim diLocal As New DirectoryInfo(Path) Dim di As DirectoryInfo If Globals.Cancelled Then Exit Sub End If Try ' Handle subfolders, if required. If Me.SearchSubfolders Then For Each di In diLocal.GetDirectories If (File.GetAttributes(di.FullName) And _ FileAttributes.ReparsePoint) <> _ FileAttributes.ReparsePoint Then Search(di.FullName) End If Next End If ' Search for matching file names. Dim fi As FileInfo Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi In afi If Globals.Cancelled Then Exit For End If OnFileFound(fi) Next alFiles.AddRange(afi) Catch ex As UnauthorizedAccessException ' Don't do anything at all, just quietly get out. This means you ' weren't meant to be in this folder. All other exceptions will ' bubble back out to the caller. End Try End Sub

Using a Second Thread

The second button, labeled Incorrect Async, attempts to fix the thread blocking issue by running the FileSearch.Execute method in a background thread. The code adds this behavior by first creating a delegate type with a signature that matches that of FileSearch.Execute so that it can refer to the FileSearch.Execute method:

Private Delegate Function ExecuteDelegate() As ArrayList

Instead of calling the FileSearch.Execute method directly, the second version of the code creates an instance of the ExecuteDelegate type containing a reference to the Execute method, and calls the BeginInvoke method of the delegate to run the code on a secondary thread, as shown in the following:

Dim ed As New ExecuteDelegate(AddressOf fs.Execute) AddHandler fs.FileFound, AddressOf EventHandler ed.BeginInvoke(AddressOf HandleCallback, ed)

The HandleCallback procedure retrieves the original delegate instance from the state information passed to the procedure, and calls the EndInvoke method:

' After the async search has completed, run this code. If you call ' BeginInvoke, you generally must call EndInvoke as well. Dim ed As ExecuteDelegate = DirectCast(ar.AsyncState, ExecuteDelegate) Dim al As ArrayList = ed.EndInvoke(ar) ' You could use the ArrayList here, if you like.

Try running the sample again, clicking Incorrect Async, and you'll see that the form's user interface remains responsive while the search is running. Clicking Cancel Search now cancels the operation, and you can move or resize the form as the code is running. In addition, the form includes an event handler for the Form.Closing event, ensuring that if you attempt to close the form, the code sets the Globals.Cancelled flag first. This avoids any unpleasantness that might occur when the .NET runtime attempts to call the form's HandleCallback procedure after the form has been removed from memory.

Figure 3 Using Background Threads

Figure 3** Using Background Threads **

Unfortunately, although this solution appears to solve the problem, it breaks a cardinal rule of Windows-based applications and multithreaded programming: "Thou shalt not interact with a control's properties from a thread other than the one that created the control." (The Form class inherits from the Control class, so the same issues apply when referring to properties of a form.) The corollary to the prime directive is that every procedure runs in the thread from which it was called. Because the EventHandler procedure is called from code running in a secondary thread, it too runs in the secondary thread. The EventHandler procedure updates the form in several ways, breaking the all-important rule. You can verify this transgression by examining the contents of the output window in the sample application. Debugging code within the sample writes the current thread ID into the window, and you can see that the active thread while writing to the form is different from the form's thread. Figure 3 shows output after running the sample application.

Using Invoke to Avoid Thread Contention

Although the demonstrated breach of protocol may not show any symptoms in the sample application, it can cause problems for you in real applications when multiple threads contend for a single resource (such as a property of a control or of the form itself). The accepted solution in Visual Basic .NET 2003 is to use the form's Invoke method inside the event handler (which will always run on a secondary thread in this example because it's called from the secondary thread) to perform a thread switch back to the form's thread. The Invoke method takes care of the problem. As described in the documentation, the method "executes the specified delegate on the thread that owns the control's underlying window handle."

You can run a third example that demonstrates this behavior by clicking Better Async on the form. This version calls the event handler named EventHandlerAsync each time a file is found. In this handler, instead of calling the UpdateDisplay method directly, the event handler uses a delegate type created in the form's class to invoke the UpdateDisplay method indirectly, in the form's thread:

' In the form's class: Private Delegate Sub UpdateDisplayDelegate( _ ByVal Text As String, ByVal Value As Long) ' In the EventHandlerAsync procedure: ' UpdateDisplay(e.FileFound.FullName, e.Counter) Dim atd As New UpdateDisplayDelegate(AddressOf UpdateDisplay) Me.Invoke(atd, New Object() {e.FileFound.FullName, e.Counter})

To observe this safer behavior, run the sample again, and view the debugging information in the output window. Figure 4 shows sample output, demonstrating that it's possible, with enough effort, to execute code called from a background thread without breaking the rules.

Figure 4 Executing Code from a Background Thread

Figure 4** Executing Code from a Background Thread **

As an aside, note that updating the UI for every file found, as I'm doing here, can cause the UI to be flooded if files are found frequently enough. This would happen if files are found too quickly for the UI to process them effectively. You can overcome this situation in your own applications by batching the updates.

Obviously, 1,000 words on the topic of Windows Forms and asynchronous behavior barely begin to do justice to this complex topic. I've presented the .Microsoft® NET Framework 1.x version of the sample merely to provide the backdrop for what's to come in the .NET Framework 2.0—the BackgroundWorker component.

Introducing BackgroundWorker

The BackgroundWorker component is an implementation of a new event-based pattern, also exposed by Web service proxies and System.Net.WebClient in the .NET Framework 2.0. BackgroundWorker makes it possible to handle asynchronous operations on Windows Forms without requiring you to lose sleep about cross-thread disasters lurking in the distance and without requiring delegate creation and invocation. This new component exposes an event and property model that makes it easy for any developer to take advantage of asynchronous features.

Of course, there's no free lunch—using the BackgroundWorker component requires you to work the way the component wants you to work, and forces you to rethink your application in order to fit into its very narrow view of the world. You will never have the same flexibility using the BackgroundWorker component that you would have if you created and managed your own threads. On the other hand, you don't have the intricacies, difficulties, and potential potholes, either. And the resulting code is often much cleaner.

Converting the previous example so that it takes advantage of the new BackgroundWorker component was relatively simple. After doing the research into how the BackgroundWorker component operated, the conversion took about 10 to 15 minutes. It also allowed me to remove large chunks of the original code.

I started by opening the project in design mode. From the Components tab on the Toolbox window, I dragged an instance of the BackgroundWorker component to the form. For this sample, I renamed the component so its Name property was bgw. If you examine the control in Design view, you'll find two important properties in the Properties window: WorkerReportsProgress and WorkerSupportsCancellation. The sample sets both of the properties programmatically. Without setting these properties to True either in the Properties window or in code, a user won't be able to cancel the background process and the app won't be able to report on the status of the process.

To get the BackgroundWorker component "started," simply call its RunWorkerAsync method, like this:

Dim fs As New FileSearch(Me.cboDrives.Text, Me.txtFileSpec.Text, _ Me.chkSearchSubfolders.Checked) ' Start the background process, in a separate thread. bgw.RunWorkerAsync(fs)

The RunWorkerAsync method, normally run in the form's thread, causes the component to grab a background thread from the thread pool, and then triggers the BackgroundWorker's DoWork event handler, which runs in that background thread. In this procedure you add your code that does work asynchronously, in the background thread. The DoWork event handler might include code like that shown in Figure 5. (This isn't the exact code used in the demo because the Execute method needs to be altered to support cancellation—more on that later.)

Figure 5 DoWork Event Handler

Private Sub bgw_DoWork(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgw.DoWork 'This method will run on a thread other than the UI thread. 'Be sure not to manipulate any Windows Forms controls created 'on the UI thread from this method. ' Retrieve the FileSearch object passed into the procedure, ' and then set up its event handler: Dim fs As FileSearch = CType(e.Argument, FileSearch) AddHandler fs.FileFound, AddressOf EventHandler ' Execute the search: fs.Execute() End Sub

Because this procedure's code runs in a different thread than the form's thread, it's imperative that you don't interact with any member of the form or its controls from this code.

When the work has been completed, BackgroundWorker raises its RunWorkerCompleted event, allowing you to clean up. This procedure runs in the same thread as the form, so you can again access user interface elements with impunity. Your event handler might look like the following code snippet (again, the final code will be modified to support cancellation):

Private Sub bgw_RunWorkerCompleted(ByVal sender As Object, _ ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _ Handles bgw.RunWorkerCompleted ' The background process has completed. This procedure ' runs in the same thread as the host form. Me.lblResults.Text = "Search complete" End Sub

Of course, you haven't yet seen code that actually does something once you've found a file. In the previous example, the code added the file name to the listbox on the form in code that was called from the FileSearch.FileFound event handler. The BackgroundWorker component allows for the same behavior, but instead of doing the work directly (as in the earlier "bad" example) or creating your own delegate and calling the form's Invoke method to run it, the BackgroundWorker component handles this thread switch in an elegant fashion. The BackgroundWorker component allows you to call its ReportProgress method from the background thread, which triggers its ProgressChanged event handler back in the form's thread.

You don't have to handle the thread switch yourself, using the delegate/Invoke method; instead, call ReportProgress, and the component does the rest. There's one important caveat, however: the component only raises the ProgressChanged event if you have set the component's WorkerReportsProgress property to True. Without setting that property, the component will never call your event-handling code, so make sure to set it.

When you call the ReportProgress method, you supply an integer between 0 and 100 that indicates the percentage of the background activity that has completed. You can also supply any object as a second parameter, allowing you to pass state information to the event handler. Both the percent and your own object (if supplied) get passed to the ProgressChanged event handler as properties of the ProgressChangedEventArgs parameter passed to the procedure. The properties are named ProgressPercentage and UserState, respectively, and your event handler can use them in any way it requires. The sample application calls the ReportProgress method from its FileSearch.FileFound event handler:

Private Sub EventHandler( _ ByVal sender As Object, ByVal e As FileFoundEventArgs) ' A file was found. Report the progress, triggering the ' BackgroundWorker.ProgressChanged event: Dim intPercent As Integer = _ CInt((e.Counter - pbStatus.Minimum) Mod pbStatus.Maximum) bgw.ReportProgress(intPercent, e) End Sub

The sample application uses the ProgressChanged event to update the display as each file is found (see Figure 6).

Figure 6 Using ProgressChanged to Update the Display

Private Sub bgw_ProgressChanged(ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles bgw.ProgressChanged ' The BackgroundWorker.ReportProgress method was called. ' This procedure runs in the same thread as the host form. ' Retrieve the FileFoundEventArgs reference, and display the file name, ' the number of found files, and the current progress percentage: Dim state As FileFoundEventArgs = _ CType(e.UserState, FileFoundEventArgs) UpdateDisplay(state.FileFound.FullName, _ state.Counter, e.ProgressPercentage) End Sub

The UpdateDisplay procedure simply takes the information and updates the form's content, as shown here:

Public Sub UpdateDisplay( _ ByVal Item As String, ByVal Counter As Long, ByVal Value As Integer) ' Add the item to the list box, update the status bar, ' and display the number of found files in the label control. lstResults.Items.Add(Item) pbStatus.Value = Value lblCounter.Text = String.Format("{0} file(s) found", Counter) Me.Update() End Sub

As you can see, it takes no extra effort to get asynchronous behavior—there's no need to create a delegate type or to invoke a delegate instance. However, there is still one unresolved issue: how do you cancel the search? In theory, this is easy.

Canceling the Search

Once you set the WorkerSupportsCancellation property of the BackgroundWorker component to True, you can cancel the asynchronous operation, calling the BackgroundWorker's CancelAsync method like so:

Private Sub btnCancel_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnCancel.Click ' Cancel the async operation: bgw.CancelAsync() End Sub

Just as with the WorkerReportsProgress method, unless you explicitly set the WorkerSupportsCancellation property to True, you'll never successfully cancel the operation. (In this version of the sample, the Form.Closing event handler also calls the CancelAsync method, ensuring that closing the form cancels the asynchronous operation if it's running.)

By calling the CancelAsync method, all you've really done is signal to the component that you want to cancel any pending operations. It's up to your code to stop doing its work, by checking the CancellationPending property of the BackgroundWorker component. Once you find that this flag is set, you can decide whether or not to actually suspend your operation. You must explicitly set the Cancel property of the DoWorkEventArgs object passed into the DoWork event handler to indicate that you want to cancel. Normally, you would simply include code like this to cancel the operation from within the DoWork event handler:

If bgw.CancellationPending Then e.Cancel = True ' You could perform some cleanup before ' leaving, or just exit here: Exit Sub End If

In my sample, however, this simple solution won't work. All the activity in the file search occurs in the FileSearch class and is triggered by the call to fs.Execute from inside the BackgroundWorker's DoWork event handler. One of the drawbacks to using the BackgroundWorker component is that it's difficult to extract the working code from the user interface—that is, in order to cancel the operation, you must interact with the BackgroundWorker component itself (retrieving the CancellationPending property) and the DoWorkEventArgs object passed to the DoWork event handler (setting the Cancel property). Because the FileSearch class is independent of the user interface, the code in this example needs to be modified so that the FileSearch class can "see" the BackgroundWorker component and the DoWorkEventArgs object.

To solve this problem, I modified the FileSearch.Execute method to take as its parameters the two necessary object references. The procedure stores the references in class-level variables for use during the search operation. The FileSearch class adds the two class-level variables shown in the following:

' Keep track of BackgroundWorker and DoWorkEventArgs, ' so this code can cancel or report progress: Private bgw As System.ComponentModel.BackgroundWorker Private eventArgs As System.ComponentModel.DoWorkEventArgs

The FileSearch.Execute method takes the values and stores them in the class variables, as shown here:

Public Function Execute( _ ByVal backGroundWorker As System.ComponentModel.BackgroundWorker, _ ByVal e As System.ComponentModel.DoWorkEventArgs) As ArrayList ' Store the reference to the BackgroundWorker and the ' DoWorkEventArgs objects: bgw = backGroundWorker eventArgs = e ' Search for matching files. Search(LookIn) Return alFiles End Function

Finally, the FileSearch.Search method uses the class-level variables to determine whether it should cancel its search, and indicates to the BackgroundWorker component that it has quit (see the code in Figure 7).

Figure 7 Should It Cancel?

Private Sub Search(ByVal Path As String) Dim diLocal As New DirectoryInfo(Path) Dim di As DirectoryInfo ' Cancellation pending? Cancel and get out. If bgw.CancellationPending Then eventArgs.Cancel = True Exit Sub End If Try ' Handle subfolders, if required. If Me.SearchSubfolders Then For Each di In diLocal.GetDirectories If (File.GetAttributes(di.FullName) And _ FileAttributes.ReparsePoint) <> _ FileAttributes.ReparsePoint Then Search(di.FullName) End If Next End If ' Search for matching file names. Dim fi As FileInfo Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi In afi ' Cancellation pending? Cancel and get out. If bgw.CancellationPending Then eventArgs.Cancel = True Exit For End If OnFileFound(fi) Next alFiles.AddRange(afi) Catch ex As UnauthorizedAccessException ' Don't do anything at all, just quietly get out. This means you ' weren't meant to be in this folder. All other exceptions will ' bubble back out to the caller. End Try End Sub

Although it seems that using the BackgroundWorker component takes a lot of effort, it really doesn't. This particular example showcases most of the features of the component, but you don't need to use them all in every application you write. One thing's for sure: if you're creating Windows-based applications that require asynchronous behavior, and you don't need explicit control over the background operation, using the BackgroundWorker component can save you time and relieve you from the effort of creating your own delegates and invoking them. It doesn't really get much easier than that.

Send your questions and comments for Ken to  basics@microsoft.com.

Ken Getz is a senior consultant with MCW Technologies. He is coauthor of ASP.NET Developer's JumpStart (Addison-Wesley, 2002), Access Developer's Handbook (Sybex, 2001), and VBA Developer's Handbook, 2nd Edition (Sybex, 2001). Reach him at keng@mcwtech.com.