2004-04-20
| Rate This Article: | Add This Article To: |
As I type this article into my computer, it is busy with a couple of other tasks—printing a photo on my inkjet and downloading some trial software from the Internet. It's cool, not to mention useful, to do three things at the same time! However, this apparent multitasking is just an illusion, because any single-CPU computer is limited to doing one thing at a time. This magic trick is pulled off using threads, and an understanding of how threads work and how to use them in your .Net programs belongs in every programmer's skill set.
Thread Basics
When your computer appears to be doing multiple things at once, what is really happening is that the CPU's time is being divided up between the various tasks. Task 1 — printing, for example — gets the CPU for some fraction of a second, then task 2, task 3, and so on. Because the computer is very fast and human perceptions are relatively slow it seems as if the tasks are being done simultaneously. This ability to share the CPU among multiple tasks is called multitasking, and it is a feature of the Windows operating system (and most other operating systems too).
The thread is the basic unit of multitasking, the entity to which the operating system assigns CPU time. Each program typically has a single thread, as do various background processes associated with the operating system. The operating system assigns CPU time to the running threads based on their priority. In Windows, a thread can have one of four priorities as described here from highest to lowest:
- Real time: used for processes that cannot be interrupted, such as games with complex graphics and streaming video.
- High: used for time-critical processes that must execute essentially immediately for proper function. The Windows Task manager is an example.
- Normal: used for processes that have no special CPU scheduling demands. Most application programs, such as a word processor or spreadsheet, are assigned this priority.
- Idle: used for processes that should run only when no higher priority process is active, in other words when the system is idle.
How does threading relate to the .Net programmer? A .Net program by default has a single thread, but you can create two or more threads within a single program if you desire. Because these threads are all part of the same program, or process, they all have access to the program's resources (global, static, and instance fields, for example). But because Windows allocates CPU time based on threads, not processes, a multithreaded program will get more than its normal share of CPU time.
What exactly does this mean in practical terms — does the program run faster? Generally speaking, no, at least on single processor computers. (Multiple processor systems are another story and beyond the scope of this article). It means, rather, that the program (or some programs, anyway) can be more responsive to the user. When a program has only a single thread, that thread is responsible for all program operations, including responding to user input, such as typing text into a text box, selecting menu commands, and clicking buttons. If the thread is occupied with long-running data processing, such as mathematical calculations or XSLT processing, response to the user can become sluggish. Click a menu title, for example, and it may take several seconds for the menu to appear. This can be very frustrating to the user, and a competent programmer will see that it never happens. By creating a second thread and assigning the long-running process to it, the programs initial, default thread can respond immediately to user input.
The Downside
So far, multithreading may sound like a free lunch, but as we all know there is no such thing. Let's look at a couple of the downsides of multithreading.
One downside is that thread switching consumes resources. Each time the CPU switches from one thread to another, the state of the current thread must be saved, so it can be resumed later, and the state of the new thread must be retrieved and loaded into the CPU registers. Just adding one extra thread to improve responsiveness is not going to cause any noticeable problems, but if you use, say, 20 threads in a complex statistical analysis program, the extra overhead of task switching may nullify the theoretical advantages of multithreading.
The second downside is that programming multiple threads can be complex. One problem that faces programmers is called a deadlock, when two threads are each waiting for the other to complete, resulting in neither thread ever completing. A related problem, called a race, results when the program's operation depends on which of two or more threads completes first. While .Net provides classes and tools to deal with these and other problems of thread management and synchronization, it remains a complex business.
The bottom line, for me at least, is to use multithreading only when there is a clear advantage to doing so, and then to use as few threads as possible. The good news is that these ugly problems are not a factor when you use multithreading to improve program responsiveness.
Implementing Multithreading
The .Net Framework offers two ways to create a new thread in your program. The remainder of this article explains the simpler of these two methods, the thread pool, which is perfectly adequate for many uses. The .Net runtime maintains a queue of idle threads and assigns them to programs when requested. When the code that the program executes in the thread is complete, the thread is returned to the pool. This is why this method is so simple; the runtime takes care of most of the details for you via the ThreadPool class.
All ThreadPool threads have normal priority, which is what you want when you are using a second thread to improve program responsiveness. The ThreadPool class and all other multithreading-related classes are in the System.Threading namespace.
To create a new thread, call the static ThreadPool.QueueUserWorkItem method. The syntax is as follows:
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadCode), Done);
- ThreadCode is the name of the method in your program where the new thread should start and end executing.
- Done is an AutoResetEvent object that you use to signal the thread pool manager that the program is finished using the thread.
The method specified by the ThreadCode argument must have the proper signature, matching that of the WaitCallback class. Specifically, it must take one argument of type Object and have a void return value. Code in this procedure must also inform the thread pool that it is finished. Here's an example shows a method that meets these two requirements:
static void MyThreadCode(object state)
{
//Code to be executed by the thread goes here, including
//calls to other methods as required. Execution must return
//here when complete.
//Tell the thread pool that the thread has finished.
((AutoResetEvent)state).Set();
}
You would create this thread and start it running as shown here:
AutoResetEvent IsDone = new AutoResetEvent(false); ThreadPool.QueueUserWorkItem (new WaitCallback(MyThreadCode), IsDone);
Seems too simple to be true, right? Perhaps the following demonstration will convince you!
A Demonstration
This simple program shows how easy it is to implement multitasking to improve program responsiveness. The code mimics a long calculation or other data processing task with a loop that runs for 10 seconds. You can run this loop either within the program's single default thread, or you can run it in a new thread created using the techniques just described. While the calculations are running you can click a button on the form to display a message box. You'll see a marked difference in how quickly that message box displays with one or two threads.
To create the demo, start a new C# windows Application project. Place four button controls one above the other on the form, with the default names button1 through button 4 top to bottom. Change the captions as shown in the figure.
Next, put the statement
using System.Threading;
at the beginning of the code along with the other using statements. Finally, add the code shown in Listing 1 to the program. When you run the program, it will be very clear how much difference a second thread can make!
Listing 1. Code for the multithreading demo program.
private void button1_Click(object sender, System.EventArgs e)
{
//Enable/disable buttons.
button1.Enabled=false;
button2.Enabled=false;
button3.Enabled=true;
button4.Enabled=false;
LongCalculationOneThread();
}
private void button2_Click(object sender, System.EventArgs e)
{
//Enable/disable buttons.
button1.Enabled=false;
button2.Enabled=false;
button3.Enabled=true;
button4.Enabled=false;
//Run in new thread.
AutoResetEvent isDone = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(new WaitCallback(LongCalculationTwoThreads), isDone); }
private void button3_Click(object sender, System.EventArgs e)
{
MessageBox.Show("You clicked the button!");
}
private void button4_Click(object sender, System.EventArgs e)
{
this.Close();
}
private void LongCalculationOneThread()
{
// Loop for 10 seconds.
DateTime t = DateTime.Now;
t = t.AddSeconds(10);
while (t > DateTime.Now);
// Reset buttons.
button1.Enabled=true;
button2.Enabled=true;
button3.Enabled=false;
button4.Enabled=true;
MessageBox.Show("Calculations finished.");
}
private void LongCalculationTwoThreads(object state)
{
// Loop for 10 seconds.
DateTime t = DateTime.Now;
t = t.AddSeconds(10);
while (t > DateTime.Now);
//Reset buttons.
button1.Enabled=true;
button2.Enabled=true;
button3.Enabled=false;
button4.Enabled=true;
// Signal that thread is done.
((AutoResetEvent)state).Set();
MessageBox.Show("Calculations finished.");
}
|
![]() |
|


