One of the many new wrinkles in Microsoft's .NET Framework is the inclusion of an easy, standardized way to create free-threaded applications. Since VB6 wouldn't let you create multiple threads in an application without some external helper code, this language feature is new to many of the programmers who will be working with .NET. In this article, I'll introduce you to the .NET Framework's support for multithreaded applications.
First, for the benefit of my friends in VB-land, I'm going to give a brief and somewhat simplistic explanation of what a thread is and how it relates to an application. If you've done C++, Delphi, or Java development, you may want to skip this part, since these languages have been free-threaded for some time.
Applications, processes, and threads
Every running Windows application has a process, which logically represents the application's memory address space into which DLLs are loaded and variables are stored. By itself, a process doesn't really do anything except define that address space. For any work to be done, a process needs an execution path, or a thread. Multiple threads can be running in a single process, and these threads all share the host process' address space. (This is important, and I'll have more on it later. Windows will divide a process' CPU time among multiple threads in much the same way that it divides time up among multiple processes. This essentially gives a process the ability to do multiple tasks at the same time.
The Thread object
Okay, all the non-VB members can come back now. The .NET Framework exposes an object-oriented API for creating and working with threads that is deceptively simple to use. The Thread object lives in the System.Threading namespace. Each instance of Thread represents a different thread within the application's process. Figure A lists some important methods and properties of Thread.
Thread's constructor accepts the address of a method that should be executed on the new thread once it is running. This address is referred to in Microsoft's documentation as a "delegate" or "delegate method." To create a delegate in VB.NET, you use the AddressOf keyword on a method, while in C# you create a new ThreadStart object. COM programmers might expect that a delegate would have to be an instance method, but this is not the case with .NET. It doesn't matter if the method passed to the constructor is static (shared) or an instance method; either one will do the trick.
To start executing the delegate on the new thread, call the Start method. Believe me, it's easy to overlook this step, so check that you have called Start before pulling your hair out because your delegate isn't being called. Start will return immediately, but there is some overhead in getting a new thread started, so don't assume that your thread is up and running just because your main code has resumed. Check the thread's IsAlive property to see whether it has started successfully. Once a thread is running, it will continue to execute until its delegate method ends, its process is ended, or it is stopped by another thread.
Get your priorities straight
Threads are given scheduling priorities for CPU time, in much the same way that applications are given them. A thread's Priority property controls how large a proportion of its host process' CPU time slice it is given. These settings, from largest to smallest, are:
- · Highest
- · AboveNormal
- · Normal
- · BelowNormal
- · Lowest
Remember that this setting controls the proportion of the finite amount of a process' CPU time that's given to a thread. Thus, a thread with a priority of Highest may not leave much time for the other threads in the process to do anything. Generally, threads that control aspects of the UI should be given Normal priority, and those with no UI elements should have lower priorities so that your application's UI will remain responsive to the user.
Controlling the action
Once your new thread is up and running, you can exert some control over it through the methods of its instance variable. To see what your thread is currently doing, check its ThreadState property. ThreadState returns an enumerated type that describes the current state of the thread.
The Sleep method causes the thread to "go to sleep" for the amount of time you specify. While sleeping, a thread doesn't consume any CPU cycles, so sleeping is an efficient way of making a thread wait for a resource to become available or for some work to do. While sleeping, a thread's ThreadState will return WaitSleepJoin.
If your thread has slept long enough, you can wake it up with the Interrupt method. Calling Interrupt on a thread will throw a ThreadInterruptedException in the code that the thread was executing. So if your thread needs to know when it has been put to sleep, set a Catch for that exception.
Interrupt has a rather counterintuitive name, in that you can't interrupt a thread unless it has been sleeping. If the thread is awake when interrupted, the interrupt will queue until the next time the thread is told to sleep, when it will then immediately be interrupted. This will probably not be what you want, so verify that a thread is in WaitSleepJoin state before interrupting it.
You can also pause a thread with the intention of resuming it later. The Suspend method suspends execution on the thread until you call Resume. A thread may not suspend itself immediately, and until it does, its ThreadState will be SuspendRequested. Once it has been suspended, ThreadState will return Suspended. So what's the difference between Suspend/Resume and Sleep/Interrupt? The two biggest differences are:
- · A Sleep request occurs immediately, while a Suspend is queued until the next time Windows transfers CPU time to another thread or process.
- · A thread is notified of an Interrupt via an exception, while a resumed thread has no idea that it has been suspended and resumed.
To stop and dispose of a thread immediately, call the Abort method. Abort kills the thread by throwing a ThreadAbortException in its delegate. This is not a catchable exception, but if it is thrown from within a Try… Finally block, the Finally code will be executed before the thread is ended.
There is no Stop method
You're likely to find books, articles, and documentation that refer to a Stop method as an alternate way of ending a thread. This method was removed in Beta 2 of the .NET Framework.
One other method deserves special mention here. The Join method will suspend the currently executing thread until the referenced thread ends. For example, given the following C# code:
//create delegate for Class1.DoSomething
delegate = new ThreadStart(Class1.DoSomething);
Thread t = new Thread(del); //create new thread
The call to Console.Writeline will not execute until after t has finished executing Class1.DoSomething. You use Join when work on one thread cannot complete until work on a second thread has finished, or when you are waiting for all other threads to terminate so you can end your application. A thread that is waiting to Join another thread will have a ThreadState of WaitSleepJoin.
So when do I use this stuff?
One very good example of where a multithreaded approach can be helpful is when performing a long-running task that a user might want to cancel before its completion. That can be difficult to pull off on a single thread, but by running the task on a separate thread, it's a snap to keep the UI responsive. For an example, see the VB.NET code in Listing 1.
In a situation where multiple clients will need access to the same objects concurrently, spawning multiple threads to handle those requests can improve an application's performance. An application that communicates asynchronously with a database, message queue, the file system, or another application can also benefit from a multithreaded approach.
The flip side
You say there's got to be a catch? You're right. Actually, there are a couple:
- · Remember that there is CPU overhead involved in creating and switching between multiple threads. Since a process' CPU time is finite, many concurrently executing threads may actually cause an application to perform more slowly than a single threaded application performing the same task.
- · Because threads all have access to the same memory address space, they have the potential to access globally visible variables at the same time. This can have unexpected consequences that can create difficult-to-isolate bugs in your application.
A prescription for the first issue would include careful planning and benchmarking to ensure optimum performance. The solution to the second potential problem leads to a topic called synchronization, or preventing multiple threads from accessing the same resource concurrently. This will be the topic for a future article.
By now you should have enough information to begin experimenting with .NET's threading support. Just be sure not to run with the scissors.
Weaving an application
What tips or advice do you have for someone wanting to develop a multithreaded application? Send us an e-mail with your suggestions and experiences or post a comment below.