Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Asynchronous Programming in VB.NET


April 2002/Frameworks

Microsoft calls Visual Basic.Net a “first class” object-oriented programming language. Microsoft also suggests that your programming language preference is now a lifestyle choice rather than a technical choice. I agree with them, and in this article I will show you just one of the reasons first class-dom is more than marketing hype for VB.NET.

A first class object-oriented language in the millennial sense of the word means that a language must support inheritance, interfaces, aggregation, encapsulation, association, and polymorphism. VB6 supported most of these things, but VB.NET now supports all of them, with the incorporation of inheritance into Visual Basic.Net. But, more than the basic six aspects of an object-oriented programming language, Visual Basic.Net supports idioms that we have come to demand of first class languages. I am referring specifically to capabilities like multithreading.

Businesses choose languages based on historical preference, available labor pools, and sometimes recommendations from developers; but developers choose languages based on what their boss said to use, familiarity with a language, or the capabilities of the language. It is the latter reason — capabilities of a language — that programmers might have chosen C++ or Delphi over Visual Basic. Historically, Visual Basic provided adequate support for asynchronous processing through simple devices like the timer control, but for threads you were out of luck for the most part. Consequently, if you needed to perform asynchronous processing then you chose something else besides Visual Basic 6.

This sad state of affairs no longer exists. Visual Basic.Net supports four different ways of implementing asynchronous processing, from the continuous support of controls like the Timer control to multithreaded support provided by the Thread class. You heard it correctly: Visual Basic.Net supports multithreading. In addition to multithreading, Visual Basic supports asynchronous processing, and simplified threading using thread pooling.

In this column, I will demonstrate all four means of employing asynchronous processing, included using event-driven controls, asynchronous method invocation using delegates (function pointers), thread pooling, and the Thread class, for ultimate control. I will also demonstrate a couple of simulated scenarios, which elaborate on when you should pick one over the other. We will proceed from the most basic and easy to implement technique of using the Timer control’s Tick event to the most advanced technique, which demonstrates how to use the Thread class.

Using the Tick and Idle Events

When you need to perform very lightweight, secondary tasks you have two excellent choices. You can use the Timer.Tick event carried over from VB6, or you can use the Application object’s Idle event, introduced in Visual Basic.Net.

I’m assuming that discussing the Timer.Tick event will be review for many readers, but it provides some useful background leading into the new capabilities in VB.Net. The Timer control is based on the BIOS’ interrupt 1Ch. This interrupt occurs at regular intervals indicated by the oscillating crystal in the CPU. Ten or fifteen years ago we would use function pointers to replace the address of the 1Ch interrupt to point to our procedure in order to catch the timer interrupt. For years now, we’ve had the Timer control provide this facility for us.

To use the Timer.Tick event in Visual Basic.Net, drag a Timer control to the component tray from the toolbox. Double-click on the Timer control to generate the default event handler, Tick, and add your code. (See Figure 1 for a picture of the component tray.)

To use the Timer component we have to set the Timer.Enabled property to True and the Timer.Interval property to the number of milliseconds that we would like to wait before the component invokes the Tick event. Finally, we need to add some code to the Timer.Tick event handler. (This is the model for Windows programming: associate the GUI with code via event handlers.)

Suitable code for a Timer event should be very short and something that needs to happen at regular intervals. A perfect example is to display the system time in a statusbar at the bottom of a form. Displaying the time is a simple operation, and looks best if it happens at regular intervals. The code to display the system time is shown in Example 1.

The code in Example 1 assumes you have a basic Windows Application in Visual Basic.Net. You have added a Timer component, named Timer1 by default, added to the form, and you have generated the Tick event handler by double-clicking on the Timer component. TimeString is a property defined in the System namespace. The code in Example 1 displays the time in an hh:mm:ss format, and updates the time value at the interval prescribed by the Timer.Interval property.

Using the Application Object’s Idle Event

If you look in the Task Manager for Windows NT-based systems you will see that most applications spend a considerable amount of time idle. For lightweight processing that can happen suitably at irregular intervals use the Application.Idle event. However, we do not want to bog the system down in the Idle event with background tasks because at any moment a user might request a foreground task. For this reason — to avoid annoying users — background Idle tasks should be short.

We can implement the system time behavior using the Idle event. The result will be irregular timekeeping, but the result is functional.

Every Windows Forms-based program in .Net is represented by a Singleton Application object. (This is an idea conceived in Borland’s Object Pascal and Delphi and introduced recently in VB.NET.) Although there is no Application component that we can pick from the toolbox in VB.NET; we just have to understand that it is there. Because Application is not a component yet, a little more effort is required to wire the Application.Idle event up to an event handler.

The first thing you will need to know is that events are presented as function pointers and event handlers are the functions pointed to. The second thing you will need to know is that event handlers are called delegates in .Net and are associated with events using the AddHandler statement. Since it is reasonable to assume that we want the system time to start displaying when the form showing the time is visible, we need to wire the event handler when the form loads. Example 2 demonstrates how to associate an event handler with an event in VB.NET.

Form1_Load represents the Load event handler for a Windows Form in VB.NET. (The underscore continuation character from VB should go away, eventually.) The AddHandler statement is read as assign the AddressOf OnIdle to the event Application.Idle . (This is an example of where operator overloading would be useful, resulting in a more intuitive Appication.idle = OnIdle statement. I expect to see operator overloading in the very near future for VB.NET.)

Now when the Idle event occurs in the application, the procedure OnIdle will be called and the timer will be updated. Depending on how busy the application is will determine how frequently the time is updated. When the application is not very busy, the time should be updated pretty regularly. When the application is very busy, the time may not be updated for a few seconds. In the latter example the user’s attention will be elsewhere, and the time probably will not be that significant.

The Timer.Tick and Application.Idle events really occur synchronously. These event handlers will prevent other code from running until they return; if the Tick or Idle handlers are long then your application may appear sluggish or unresponsive. If this weren’t the case, you would have the same problems with Timer.Tick and Application.Idle that can occur with asynchronous or threaded calls. It is because Tick and Idle happen synchronously that they should be short. Otherwise your application will not do anything else besides processing these events. Let’s proceed now to some new capabilities available to VB.NET programmers.

Asynchronous Requests with BeginInvoke and EndInvoke

The Timer and Application object are okay for synchronous processing, but what happens if you need a task to occur in the background without holding up foreground tasks? A simple example is if you want to show a splash screen while your application is loading. A more-practical example is if you want to perform application initialization but the user can begin some tasks while initialization completes. This describes an example of an asynchronous process.

Asynchronous processes occur in a non-linear fashion. When you need lightweight synchronous processing, you can easily use the Timer.Tick or Application.Idle events. When you need lightweight, asynchronous processing, you can use BeginInvoke and EndInvoke. BeginInvoke starts an asynchronous process and returns immediately. Some times when you think you might need threads, what you actually need is a simple asynchronous process.

Here are two scenarios that demonstrate the usefulness of asynchronous processing. The first scenario displays a Splash screen while the application loads. The second scenario fills a ListBox with the numbers 1 to 100,000, representing useful work, while the application loads.

In a synchronous world, the main form would not be initialized until the Splash screen finished or the ListBox contained all of the elements. In the asynchronous world of VB.NET, the application can continue loading while the splash screen distracts the user. Or, complex initialization can occur, making the application available even though all of its features may not be available. (Who uses all of the features immediately anyway, right?!) If you employ BeginInvoke and EndInvoke together then your application can perform several asynchronous tasks and block until all tasks are complete.

Example 3 demonstrates lightweight, asynchronous processing using BeginInvoke and EndInvoke. (Line

numbers were included because the listing is a little longer than previous listings.)

The code illustrates the important and essential role that delegates (think function pointers) play in applying advanced idioms. In the example, LoadList plays the role of a worker function that is invoked as a delegate. When the form initializes in the Load event, beginning on line 10, the Application.Idle event is initialized and on lines 16 and 17 an asynchronous process is started.

Lines 16 and 17 create an instance of the delegate MethodInvoker, initializing MethodInvoker with the address of the LoadList procedure. BeginInvoke starts the asynchronous tasks represented by LoadList and immediately returns, assigning an object that implements the IAsyncResult interface to the variable named result.

We can use result electively to synchronize at a later point in the procedure. Line 19 performs some other work: in this case we show the Splash form. Line 22 blocks until the asynchronous call to LoadList returns.

In the present incarnation the form will not load until LoadList returns on line 22. If we remove line 22 then the form would load, but the list would not necessarily be completely initialized. In the example, if the work represented by LoadList was critical to allowing the user to interact with the application then we could block with EndInvoke (as in the example). If, however, the user can safely interact with the application, even though the list may not be completely loaded then we can remove the EndInvoke statement on line 22.

There is a problem with using asynchronous processes as demonstrated in the example. Asynchronous processes work on the same thread. Consequently, if we do not have the Application.DoEvents statement on line 5 of Example 3 then the list will load before the Splash screen is displayed. Because we do have the Application.DoEvents statement on line 5, the splash screen is displayed but the list isn’t actually loaded until the splash screen is finished. The result is that we are not really performing multiple, simultaneous tasks. The fix is to mix-in a second thread to offload some work to it. As the LoadList procedure represents our work to be done, we will revise the code so that initialization is done on its own thread while keeping the asynchronous splash introduction.

Employ the ThreadPool for Convenience

I asked a Microsoft program manager who works on Compact.Net why would you ever choose to create your own thread objects over thread pooling. The response was that, for the most part, you never need to. Use the ThreadPool simply because it is easier. For this to make sense you have to understand what the ThreadPool is.

The ThreadPool is a class in the System.Threading namespace. The ThreadPool class manages a dynamic collection of threads that sit around waiting to do work. When you request work to be done by the ThreadPool, it grabs an available thread and completes the work. If no available thread exists then the ThreadPool spins up an additional thread and assigns the work to be done. Managing the Thread objects is done for you automatically by the ThreadPool.

The result is that you get multithreading without the overhead of creating, starting, and managing thread objects explicitly. All other factors are the same; you just don’t have to keep track of individual thread objects. In this way, using the ThreadPool is a bit simpler and quicker because more than likely a couple of threads are sitting around waiting to be put to work. You still have to be careful when using the ThreadPool. For example, classes in the System.Windows.Forms are not thread safe just because you are using a thread from the pool. The only difference between using threads in the pool and creating a thread object is that requesting work from pool means you have offloaded the task of managing the thread object to the ThreadPool. Let’s return to our scenario now.

The purpose of using the ThreadPool is when you have a task that needs to be or that you want to be threaded. A reasonable example is an application that has a protracted initialization process. Our example simulates this by loading a ListBox with 100,000 elements at initialization. The splash screen occupies the user, but if we don’t actually initialize the system until after the splash screen finishes then we lose one benefit of the splash screen, distraction. Example 4 demonstrates using a thread in the ThreadPool to load and initialize the ListBox while the splash screen is performing its dance.

The first thing of note is that the listing is longer by approximately 50 percent. The second thing of note is that it still isn’t too long. Lines 22 through 33 implement the form’s Load event. Line 25 implements the Idle event handler (not central to our discussion) and lines 28 and 29 perform the threaded task.

ThreadPool.QueueUserWorkItem is a shared method — C++ programmers think “static” and Delphi programmers think “class”— that takes a delegate representing work to be done. The pool assigns the work to a thread and performs the work, represented by the delegate, on its own thread. You will have to be careful inside of that thread when the delegate procedure interacts with shared variables and Windows Forms controls, but there is no other work required to use the ThreadPool.

The work to be done is represented by the ThreadedLoadList procedure in lines 6 through 20. A Try...Catch...End Try exception handler is used to catch thread exceptions, such as might occur if we close the application before the thread returns. And, lines 10 through 14 load the ListBox, similar to the work performed in the earlier discussion on BeginInvoke.

The most significant change occurs on lines 11 through 14. Remember that ThreadedLoadList is on a different thread than the form’s thread (as illustrated in Figure 2). Because Windows Forms is not thread safe, we have to use the Invoke method to marshal the interaction between the thread our work is being performed on and the thread that the ListBox resides on, namely the same thread as the form.

The InvokeRequired method compares the calling thread ID to the control’s thread ID; if they are different then the code must use Invoke to push the request onto the form’s thread. (What may not be apparent is that we are calling the form’s InvokeRequired and Invoke methods, which are thread-safe.) The hardest part of all of this is marshalling the work onto the form’s thread. If we break out lines 12 and 13, this is not so complicated.

The fragment New Object() {I.ToString()} creates an array of Objects and initializes its one element to the string representation of the integer I. This array of objects is used as the arguments to the AddElem method. You have to match the number and type of arguments in Object array to the number and type of arguments in the method you are invoking. In this example we need a single string.

The fragment New AddInvoker(AddressOf AddElem) constructs a delegate of the type AddInvoker defined on line 1 and initializes it with the address of AddElem. Delegate is a class that contains an invocation list. This means a single delegate could effectively refer to multiple function pointers. This is an evolution of the unadorned function pointer idiom in C++ and the procedural type in Delphi.

Me.Invoke marshals the call to AddElem onto the form’s thread, represented by the reference to self, Me. (Me is equivalent to this in C++ and self in Delphi.)

If you step into the code, you will see that ThreadedLoadList resides on it own thread and the form and AddElem share a separate thread. When you run this example, you will note that the Splash screen performs its dance independently of the ListBox being initialized.

Maximum Control and Responsibility

You might ask, “Why would I ever create a thread object when the ThreadPool is easier and capable of performing identical tasks?” The answer is when you need ultimate control over the life of the Thread. With power comes responsibility, and creating and managing a Thread object yourself entails more work.

Assuming we wanted to create the Thread ourselves to perform our initialization work, we can use a technique similar to the one demonstrated in Example 5. This code is almost identical to that in Example 4. In the Load event the difference is we create the Thread object initializing it with the address of our procedure representing work. Thread.IsBackground will allow the application to terminate the thread when the application closes, in case the user closes the application before the thread has returned. Line 28, Thread.Start, starts the thread.

The second difference is that we call the shared property Thread.CurrentThread and the Join method to wait until the thread returns on exception. This is done in the event of an error, on lines 15 through 17.

Just as with threads in the pool, we have to be careful when using shared variables or when our thread interacts with Windows Forms controls. Care is shown by marshaling the interaction between the separate thread owning the ThreadStartProcedure and the Form’s and ListBox’s thread by using the Invoke method.

Summary

You have four powerful options when it comes to asynchronous processing in Visual Basic .NET. You can use events to perform lightweight synchronous tasks. You can use BeginInvoke and EndInvoke to perform lightweight asynchronous tasks, or you can use threads from the ThreadPool or by creating instances of the Thread class for heavyweight tasks.

Evaluate the approach that suits each specific need, using thread objects sparingly. Managing thread objects requires the greatest effort and care but can yield a lot of bang. And using the ThreadPool will provide the best performance.

Visual Basic.Net and C# share the same framework and the Common Language Runtime. This means that in addition to threads and inheritance, Visual Basic .NET has all of the power and flexibility you would expect to find in C++, making it truly a “first class” programming language.


Paul Kimmel is the founder of Software Conceptions Inc. Paul has written many books on object oriented programming, including Sams Visual Basic .NET Unleashed. Paul Kimmel provides professional architectural and programming services to companies in North America. You may contact him at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.