Asynchronous Execution in Visual Basic .NET
Brian A. Randell
MCW Technologies, LLC
Summary: Tackles one of the most common practical uses of multithreading and shows you how to execute a task on a secondary thread that does not block the main user interface (UI) thread of the application. (11 printed pages)
Note To run the sample application, you will need a version of Microsoft Windows® with version 1.0 service pack 2 (SP2) of the .NET Framework installed. All code presented in this article is Visual Basic® .NET, written and tested using Visual Studio® 2002. Testing was done on Windows XP Professional SP1 system.
With the release of Visual Basic .NET, Visual Basic developers can safely create secondary worker threads. Yes, I know there were ways around this in earlier versions of Visual Basic, but except for the Timer control, any other mechanism required tricks with the Win32 API or the use of third-party controls. In Visual Basic .NET, creating a secondary thread is as easy as this:
Dim t As New System.Threading.Thread(AddressOf SomeProc) t.Start()
Proper use of multiple threads in an application is, of course, not this easy. Multithreaded programming is hard—really hard. The goal of this sample application is to tackle one of the most common practical uses of multithreading—the ability to execute a task on a secondary thread and not block the main user interface (UI) thread of the application.
The sample application is a simple program that searches a specified drive for files that match a search specification, such as *.vb. The program itself is written as two components:
- A Windows Forms application providing the user interface in FindFiles.exe.
- The actual search for files taking place in a Class Library project and compiled as FindFilesLib.dll.
The sample application supports Debug and Release versions as is usual. The Debug version has extra code that logs certain events to a text file that you specify. (See the Appendix at the end of the article for more information.)
Remember, multithreading is a large topic. This sample application focuses on solving only one type of problem. The problem to solve is how to search for files and not block the UI. In addition, the sample application needs to support a cancel feature to end the search if the user clicks the Cancel button. For more information, see the References at the end of this article.
File Search allows you to search a specified path for a file or set of files using a specific file name or wild cards. Getting started requires that you choose either the Debug or Release version. The Debug version of the sample application requires a valid path for the debug trace file. Specify this path by setting the value of the mLogFile variable defined in the Control class. There are a few Debug.Assert statements in the Main method used to validate the value specified. In addition, the application removes the default trace listener, the Output window, so that it isn't polluted with all of the data going to the log file. Clicking the Search button after the application is loaded starts the process. By default, the application will search the local C: drive and all its subdirectories for files ending in the .vb extension. The release version works the same way excluding the creation of the log file.
You can change the drive searched by selecting it from the combo-box labeled Look In. In addition, you can type any valid drive and path combination. If you want, you can click the ellipsis button (labeled "…") to the right of the combo box to invoke the standard Windows Browse For Folder dialog. If you're interested in how this was accomplished, you can take a look at the class FolderDialog, as the dialog itself is not directly exposed by version 1.0 of the .NET Framework (Special thanks to Ken Getz for providing the code.)
Finally, you can reset the UI to its original size by selecting the Reset Size command from the View menu. Also, if you want some information about the process and the number of threads allocated by the common language runtime, you can open the sample application's About Find Files dialog box, select the Information tab, and then click on the Process Information icon as shown in Figure 1.
Figure 1. About Find Files dialog and corresponding icons
The core issue in the sample application is to have a non-blocking user interface. The first major issue to consider is how to get the search to occur on a worker thread. The next problem that many developers aren't aware of is that UI elements must only be "touched" by the thread that created them. This is a rule imposed by the current Windows GDI architecture upon which Windows Forms is built. The final two problems are how to build a class that is thread-safe and does not pummel the client with too much information, negating the benefit of the worker thread.
The .NET Framework provides rich support for multiple threads of execution. There are two interesting classes in the .NET Framework specifically related to threads:
The Thread class represents a managed thread. You can create managed threads explicitly, or use the managed threads provided by the ThreadPool. Managed threads do not necessarily correspond one-to-one with operating-system threads. The ProcessThread class, on the other hand, represents an operating-system thread. Use the ProcessThread class to obtain diagnostic and performance information.
When you work with threads in your applications, there are different personas applied to threads. It is possible for a thread to embody more than one persona at a time. Some of these personas are exposed as properties of the Thread class. Others are implied by the behavior of the thread. The following table lists some personas and additional information.
|Main Thread||The thread that starts your application. In a Windows Forms application, this is the thread that creates your forms and UI elements.||N/A|
|Worker Thread||Any non-main thread in your application.||N/A|
|Background Thread||Any thread in your application that will not keep the program alive if the main thread exits.||IsBackGround|
|Thread Pool Thread||The common language runtime automatically provides a pool of operating system threads that can be used by your programs.||IsThreadPoolThread|
|UI Thread||Another name for the Main thread in a GUI application.||N/A|
Because threads can have various personas in your application, you will often want to distinguish one thread from another. The Win32 API provides each unique thread with a 32-bit thread ID. This ID is available by accessing the shared GetCurrentThreadId function of the System.AppDomain class. This ID represents the physical OS thread. Remember, the common language runtime maps its own abstraction on top of Win32 threads. It is possible for the runtime to map multiple runtime Thread instances to one OS thread. So, although it is useful to know which OS thread is performing the work, you are more interested in the runtime Thread instance.
Accessing the current runtime Thread instance is accomplished by accessing the shared, read-only property CurrentThread exposed by the Thread class. Because multiple runtime threads can be mapped to an OS thread, you need a way to tell one thread from another. This is accomplished by giving each thread a name. Unfortunately, the only way to give a thread a name is to actually have code running on the thread. Unlike ProcessThreads, there is no way to enumerate a collection of all the managed threads in an application domain or process. Rather, you must check to see if a runtime thread has a Name (
Thread.CurrentThread.Name Is Nothing), and if not, give it a name to distinguish one instance from another.
You will find that in the Debug version of the sample application, that's exactly what is done. In most procedures, the current thread is interrogated and given a name if it doesn't already have one. If you examine the Threads window (accessible during a debugging session by means of the Debug, Windows, Threads menu), you will be able to see the current thread's ID, Name, and other information. Figure 2 shows the Threads window during a debugging session.
Figure 2. The Visual Studio .NET Threads Window During a Debugging Session of FindFiles.exe
In Figure 2, note that there are three threads shown. One is named Main Thread and another is named Thread Pool Thread:FindFiles. Both were named by the application code. The third thread doesn't have a name, but if you do some poking around, you'll find that this background thread is used by the common language runtime when it performs garbage collection. If you want to verify this, uncomment the code for FileSearch's Finalize method.
Now that you know a bit about threads in a managed application, you need to know how to actually get a worker thread started. As mentioned at the beginning of this article, you can create your own threads, but you won't be doing it in this sample application. The common language runtime team knew that developers would want to use more than one thread in their applications. To facilitate this, the runtime automatically creates a thread pool of up to 25 worker threads for each Windows Forms application. On symmetric multiprocessing (SMP) computers, there will be one thread pool per processor. Note that the runtime does not necessarily allocate all 25 threads when a program starts. So the question that should be swimming through your head is, "How do I use threads in the thread pool?"
There are a few ways to use a thread from the thread pool. You're going to access the thread pool through delegates. Delegates are sometimes referred to as type-safe function pointers. Delegates provide a way to prototype a specific method signature without knowing what type is going to provide the implementation. Delegates are useful when defining callback operations and are the foundation for Events in the .NET Framework.
In order to access a worker thread, tell the worker thread what code you would like to execute and, optionally, what piece of code you would like notified when the task completes. One of the reasons delegates are preferred over other methods is that you can pass parameters to the worker thread and the runtime takes care of all the nasty details. If you investigate creating your own threads using New Thread, you will find it's a bit of work.
The first thing you need to do is to define a delegate that matches the signature of the method you want to execute. In the sample application, you want to execute the FindFiles method of the FileSearch class. Note that either the component that contains the FileSearch class or the consuming application can define the delegate. In this sample, FindFilesLib exposes a delegate.
Here's the signature for FindFiles:
Public Sub FindFiles()
Here's the signature for its matching delegate:
Public Delegate Sub FindFilesDelegate()
That's not too difficult. The big difference is that there is code inside the FindFiles method. Delegates are abstract types like interfaces and they don't provide their own implementation. Note that the runtime does provide a bunch of plumbing to make delegates work, but you don't write any delegate-specific code.
With the delegate defined, you now can call FindFiles asynchronously. In the sample application, you will find the code to do this in the Click event for the button named
' Defined in the declarations section of frmMain Private mobjFileSearch As FileSearch ' Code omitted for clarity mobjFileSearch = New FileSearch(Me.cboLookIn.Text, Me.txtFileName.Text) Dim cb As New AsyncCallback(AddressOf Me.OnFindComplete) Dim del As New FindFilesDelegate(AddressOf mobjFileSearch.FindFiles) del.BeginInvoke(cb, del)
First, you need a reference to a valid instance of the type that will be performing the work. This is the
At this point, all that is left to do is to execute the code. As mentioned, the runtime provides the actual plumbing that makes delegates work. To execute asynchronously, call the BeginInvoke method off of the delegate variable
When BeginInvoke is executed the first time, there will be a slight delay as the worker thread is fired up. Once it has started, the runtime returns control to the calling thread, leaving the UI free to do other tasks.
With the worker thread happily on its way, the next issue is how does the worker thread notify the client when it finds a file? As with any programming problem, there are many solutions. A simple way is to fire an event. When a worker thread fires an event, the event is processed by the client code using the same thread. This becomes a problem because you want to update the UI (the ListView control) with the found file's information. As mentioned earlier, UI elements can only be "touched" by the thread that created them. The worker thread cannot put data into the ListView.
The Windows Forms designers knew this little glitch would come up, so they exposed a special delegate version of BeginInvoke off of each control (including forms, which are, in fact, glorified controls). You use BeginInvoke to perform a thread switch from the worker thread back to the UI thread where you can safely update the controls. In addition, every control exposes a Boolean property, InvokeRequired, so that you can easily check whether or not you need to switch threads.
BeginInvoke is subtly different than the previous example in that it supports two overloaded versions. Version one accepts a single parameter. A delegate reference representing the code you wish to use to update the UI. The second version accepts an additional parameter—an array of objects representing the input data. In either case, it has the same effect: switching threads. Here's a pared down example from the sample application showing it in action:
Private Sub OnFilesFound(ByVal Sender As Object, _ ByVal e As System.EventArgs) If Me.InvokeRequired Then Dim del As New UpdateUIFilesFoundDelegate( _ AddressOf Me.UpdateUIFilesFound) ' Fire and Forget Me.BeginInvoke(del Else Me.UpdateUIFilesFound() End If End Sub
A big issue for Visual Basic .NET developers moving from earlier versions is thread safety. If you created a component in Visual Basic 6.0 for example, the Visual Basic runtime protected you from the complexities of multithreaded component development. All of your code executed is in either the Main COM thread or in another single-threaded apartment (STA). The STA protected your code from simultaneous access by multiple threads. This was the default behavior.
In the .NET world, all bets are off. The code you write is inherently thread-unsafe. Your components are responsible for all thread synchronization issues. Yes, now you too can use mutexes, synclocks, and so forth, to control access to your data. Be careful what you ask for; you just might get it. (This topic however is beyond the core focus of this sample. Please see the references listed at the end of this article for more information.) The code for this sample application is written to be thread-safe using locking primitives as necessary. See the comments in the code for more information.
The final issue we need to worry about is building a polite component. The component is thread-safe and supports asynchronous execution. The final step is making sure the component doesn't overwhelm the client with too much information. If it did, it would negate the benefit of multiple threads. It turns out that the main issue is the UI itself. It is our main point of contention. You want the UI to update when there is new information from the worker task. In this sample application, that means new directories and new files found. However, the UI also needs to be able to do other tasks. If it's always busy processing new directory and new file-found events, it will never be able to respond to other demands like the user clicking the Cancel button.
The solution is to have the component expose a Boolean ClientIdle property. The client can notify the component when it's doing nothing and allow the component to raise events. If the client is busy, the component will store the interesting data, such as the file found, in some client-accessible data structure. The client can then retreive the data when it has a chance. In addition, what's elegant about this solution is that a client can run the task without listening to events at all. The client can simply wait for the callback to occur, signaling completion, and then harvest the data at the very end. This allows the component to be used in applications such as ASP.NET, where periodic events are not reasonable.
At this point you should have a pretty good idea of the issues involved in writing a UI that supports asynchronous operations, provides periodic notifications, and is responsive to the user. Let's look at some of the more unique aspects of the sample application.
As mentioned earlier, the class FileSearch fires an event when it finds a file as well as when it changes directories. The two events are listed below:
Public Event ChangeDirectory As ChangeDirectoryDelegate Public Event MoreFilesFound As System.EventHandler
Events in Visual Basic .NET use the somewhat familiar syntax provided in previous versions of Visual Basic. There are also features provided that make it even more flexible. For instance, the ChangeDirectory event is defined using a delegate:
Public Delegate Sub ChangeDirectoryDelegate(ByVal Sender As Object, _ ByVal e As ChangeDirectoryEventArgs)
The MoreFilesFound event uses a system-provided delegate System.EventHandler. The events could have been defined as follows:
Public Event ChangeDirectory(ByVal Sender As Object, _ ByVal e As ChangeDirectoryEventArgs) Public Event MoreFilesFound(ByVal Sender As Object, _ ByVal e As EventArgs)
Which way is better? It isn't really an issue of which is better. It turns out that the first way can be more flexible. The reason it was done in this sample application is to expose you to the feature. In addition, this method is more explicit in showing how events are a convenient wrapper on top of delegates.
Regardless of how the events are defined, the client application can hook the events in one of two ways. The first way is to statically bind, using WithEvents. The downside to this is that the client is always listening. With Visual Basic .NET you can dynamically hook events, as well as stop listening. This is done using the AddHandler and RemoveHandler commands. Examine the code in
OnFindComplete for examples.
To be polite, a component should not hammer the client with a constant barrage of events. In this sample application, a Queue object is used to store instances of FileInfo objects every time the server code finds a file. If the client application is interested in knowing when a file is found, the client code sets the FileSearch's ClientIdle property to True. Then when the FileSearch component finds a file in the EnumFiles method, it checks to see if the client is idle and that there is a least one FileInfo object in the queue. If both conditions are met, it fires the MoreFilesFound event using RaiseEvent. The client can the tell the server to not fire anymore events by changing the ClientIdle property to False, performing the thread switch to the UI thread by means of BeginInvoke, and letting the server continue searching.
While the server component is searching, the client application can access the queue through the read-only FilesFound property of the FileSearch instance. The client is free to read as many files as it wants. The sample has the client application read only 25 files at a time before it returns to normal processing. When it's ready to check for more files, it simply sets ClientIdle to True and the process repeats.
Notes on the Queue object
The Queue object used is built into the .NET Framework and is available in the System.Collections namespace. A queue is a great data structure as it supports adding (enqueuing) and removing (dequeuing) from the object at the same time. It's the job of the queue designer to handle multiple threads. It turns out that the .NET Framework designers did just that. You can access a synchronized version of the queue by calling the shared Synchronized method of the Queue class passing the queue instance you want synchronized as the parameter.
When the search is started, the UI tells the worker thread through BeginInvoke where it should call back to when the task is complete. One thing to remember is that the callback occurs on the worker thread. It is still necessary to perform a thread switch in order to safely finish updating the UI.
Finally, one of the major benefits of having a callback for task completion is the opportunity to check whether the process completed successfully. If you fire a delegate using BeginInvoke without specifying a callback object, there will be no way to know if the task failed. When you callback procedure is called, you can check for exceptions by calling the appropriately named EndInvoke function. You should always call EndInvoke within a Try/Catch block because any untrapped exception that occurred in the server will be thrown within your callback procedure. See the code in OnFindComplete in the sample application for details.
As you examine the code in the sample application, remember that this is a focused example in a very large world of possible scenarios. It has only touched on the possibilities of what can be done with Visual Basic .NET and the .NET Framework.
Currently I have not found any book dedicated to concurrent programming with threads and delegates with the .NET Framework or Visual Basic .NET. There are some good chapters in the following books:
- Programming Microsoft Visual Basic .NET by Francesco Balena [Microsoft Press, 2002, 0735613753]
- Essential .NET, Volume I: The Common Language Runtime by Don Box [Addison Wesley Professional, 2002, 0201734117]
Other books that do not have .NET Framework or Visual Basic .NET concepts or samples, but are worth reading include:
- Win32 Multithreaded Programming by Aaron Cohen and Mike Woodring [O'Reilly & Associates, 1998, B00007GW3Z]
- Concurrent Programming in Java: Design Principles and Pattern (2nd Edition) by Doug Lea [Addison Wesley Professional, 1999, 0201310090]
If you run the application using the Debug build, you can get a trace log showing what thread a particular piece of code is running on. This can be useful to understand what is going on inside the program. The log file is created in the Control class's Main procedure (defined in the frmMain.vb source file). Before the program will run, you need to set the value of the variable
mLogFile defined in the declaration section. Once set, the file will automatically be created when the program starts. Note also the code in Main turns OFF the default trace handler for the Debug (Output) window. See the comments for more information.
The constant MaxTraceLines defined in both frmMain and FileSearch determines how many trace lines to write out. The values are currently set at an arbitrarily low value to reduce the size of the log file. Change to your taste.
The number of log entries that will be created can be deduced using the following information:
|Application is started||Three entries for Control.Main|
|Main screen is loaded||One entry for frmMain_Load|
|User starts search||One entry for frmMain.btnSearch_Click [Starting]|
|Client starts search||One entry for FileSearch.FindFiles, which starts the search|
|Client instantiates search component||One entry for FileSearch.New when the object is created|
|Directory searched||Five entries per directory searched (FileSearch.EnumDirectory, FileSearch.EnumFiles, frmMain.OnChangeDirectory, frmMain.UpdateUINewDir, FileSearch.OnChangeDirectoryUIUpdateComplete)|
|File found||Three entries per file found (frmMain.OnFileFound, frmMain.UpdateUIFileFound, FileSearch.OnFileFoundUIUpdateComplete)|
|User cancels search||One entry for frmMain.btnSearch_Click [Cancelling] (If the user clicks cancel)|
|Server receives cancel||One entry for FileSearch.CancelRequested.Set (If the user clicks cancel)|
|Search complete||One entry for frmMain.OnFindComplete|
|User notified search is complete||One entry for frmMain.UpdateUIFindComplete|
|Application exits||Two entry for Control.OnThreadExit|
|Fatal application error||One entry for a fatal error (if any—will affect total output count in file)|