Get Started with Multithreading in .NET*

Submit New Article


Last Modified On :   October 22, 2008 3:54 PM PDT
Rate
 


by Peter G. Aitken

Introduction

Have you ever built an application where users had to wait while the application performed some lengthy calculation or operation? Learn how to improve your application's responsiveness by creating and controlling threads.

The concept of threads is central to the inner workings of most operating systems, including Windows*, but relatively few programmers even know what they are—let alone how to take advantage of them. Understanding threads, using modern operating system's multi-threading capabilities properly, is a fundamental step toward creating fast, responsive applications. The .NET Framework* makes creating multi-threaded applications easier than ever. In this article you'll see what threads are, how threading works, and how you can use them in your applications.


Why Multithreading?

To understand the power of multithreading you need to know something about how the Windows* operating system works under the hood. Windows is a preemptive multitasking operating system. The system CPU can do only one thing at a time, but to give the illusion that multiple processes are running simultaneously, the operating system splits the CPU time between the various running processes.

The term preemptive means that the operating system determines when each task executes and for how long. Preemptive multitasking differs from cooperative multitasking where each task decides how long it will execute. Preemptive multitasking prevents one task from taking up all the processor's time.

For example, you might think you can edit a word processing document, print a spreadsheet, and download an MP3 file from the Web all at the same time, but in actuality, the operating system allocates small "slices" of CPU time to each of these processes. Because the CPU is very fast, humans are very slow, and the time slices can be extremely small, all three processes appear to run simultaneously.

On a multi-processor system things are a bit more complicated, but the basic idea is the same—the operating system divides the time of the CPUs among the processes that need it. Figure 1 illustrates the principle of multitasking.


Figure 1: A multitasking operating system divides the CPU's time between all running processes.

Each process has a priority that determines how the operating system allocates CPU time to it relative to other processes. Schemes for allocating process priorities differ among OS's, but Windows 2000* has four priority levels. In descending order, these priorities are:

  • Real time: The highest priority. These processes preempt all processes with lower priority. Real time priority is reserved for processes that cannot suffer even minor interruptions, such as streaming video and games with complex graphics.
  • High priority: Used for time-critical processes that must execute immediately (or almost immediately) for proper operating system functionality. The Windows Task List is an example of a high-priority proce ss. It is assigned high priority because it must display immediately when requested by the user regardless of anything else the operating system is doing. Only real-time priority threads can interrupt a high priority thread.
  • Normal priority: Used for processes that have no special CPU scheduling needs. Word processing and background printing are examples of normal priority processes.
  • Idle priority: Used for processes that run only when the system is otherwise idle. A screen saver is a good example of this priority level.

 

These thread priorities are those defined for the Windows 2000 operating system. When you are assigning a priority to a thread in .NET*, the names that are used are different, but the meanings are clear.

Here's where the concept of threads becomes important. A thread is the unit, or entity, to which the operating system assigns CPU time. Normally a program or process is associated with a single thread. The process must accomplish everything it needs to do—drawing the user interface, responding to user input, writing files, calculating results, everything—during the CPU time that Windows allocates to its single thread. For most applications this works just fine.

Windows also supports multi-threaded processes. As the name suggests, a multi-threaded process is one in which the program divides its tasks among two or more separate threads. Because Window allocates processor time to threads and not to processes, a multi-threaded process gets more than the usual share of CPU time.

So is the purpose of multi-threading simply to speed up a program? No, although it can have that effect, particularly on a multiprocessor system. The most important use of multi-threading has to do with the program's responsiveness to users. There are few things more frustrating to users than a program that does not respond immediately to mouse and keyboard input. Yet, when a single-threaded program has lengthy calculations or I/O activities going on at the same time this is exactly what is going to happen. The program's one thread must handle user interaction and the calculations, which often causes the user interaction to become sluggish.

Therefore, when your program must respond to user input at (perceptually) the same time that it is performing one or more other tasks, you should consider multi-threading. By assigning calculations or I/O to one thread, and user interaction to another thread, you can create a program that responds to user actions efficiently while also performing the required data processing.


There's Always a Downside

Using multiple threads is not all roses, however. As with almost everything else in life, a penalty accompanies the advantages. There are several factors you should consider.

First, keeping track of and switching between threads consumes memory resources and CPU time. Each time the CPU switches to another thread, the state of the current thread must be saved (so it can be resumed again later) and the saved state of the new thread must be restored. With too many threads, any responsiveness advantages you hoped to gain may be partially nullified by the extra load placed on the system.

Second, programming with multiple threads can be complex. Creating a single extra th read to handle some background calculations is fairly straightforward, but implementing many threads is a demanding task and can be the source of many hard-to-find bugs. My approach to these potential problems is to use multithreading only when it provides a clear advantage, and then to use a few threads as possible.

Third is the question of shared resources. Because they're running in the same process, the threads of a multi-threaded program have access to that process's resources, including global, static, and instance fields. Also, threads may need to share other resources such as communications ports and file handles. You must synchronize the threads in most multi-threaded applications to prevent conflicts when accessing resources, such as deadlocks (when two threads stop as each waits for the other to terminate).

For example, suppose that Thread A is responsible for obtaining data over a network, and Thread B is responsible for performing calculations with that data. It might seem like a good idea to require Thread A to wait for Thread B to complete (so the data is not updated in the middle of a calculation), and also to require Thread B to wait for Thread A to complete (so the latest data is used in the calculations). If coded improperly, the result will be two threads that never execute. The .NET* Framework provides classes to control thread synchronization, but even so, multi-threading introduces another level of programming complexity.


Implementing Multithreading

The .NET* Framework provides two methods of implementing a new thread in your program. The first, which is easier to use but does not provide as much control of the thread, makes use of the thread pool. The thread pool is a queue of idle threads managed by the .NET runtime. To assign some code to a new thread, your program sends a request to the thread pool. The pool manager assigns an available thread to the code, executes it, and then returns the thread to the pool when the code completes. To add a thread to the thread pool, you call the static ThreadPool.QueueUserWorkItem method. Like all threading-related classes, the ThreadPool class is in the System.Threading namespace. The syntax for this method is:

ThreadPool.QueueUserWorkItem(
  new WaitCallback(ThreadCode), Done);

 

The ThreadCode argument is the name of the method in your program where the new thread should start executing. The Done argument is an AutoResetEvent object that you use to signal the thread pool manager that you're done with the thread. The method specified by the ThreadCode argument must meet the following two requirements:

  • Its signature must match that of the WaitCallback class. Specifically, it must take one argument of type Object, and must have a void return value (in VB.NET*, use a Sub method rather than a Function).
  • When the thread code completes, it must inform the thread pool that it is finished. The following example shows a method that meets the two requirements for starting thread execution:

 

static void MyThreadOperation(object state)
{
   //Code to be executed by the thread goes here, including
   //calls to other methods as required.

   //Signal that the thread has finished executing.
   ((AutoResetEvent)state).Set();
}

 

You would start this thread running with the following code:

AutoResetEvent IsDone = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem
  (new WaitCallback(MyThreadOperation), IsDone);

 


A Demonstration

The following short program demonstrates how to create and use a thread through the thread pool, and it also illustrates how multithreading can improve a program's responsiveness. The plan is to create a time-consuming process that can be run two ways—as a separate thread or as part of the program's default thread. This process mimics complex calculations or I/O for which a real-world program might use multithreading. You can elect to run the time-consuming process on a single thread or in its own thread. While the long process is running, you can still edit the text in a text box control to experiment with the difference in the program's responsiveness to user input when you add the second thread. Figure 2 shows the sample application's interface.




Figure 2: the sample application demonstrates the use of multithreading by letting you compare a multi-threaded approach with a standard single-threaded application design.

The application uses a loop that executes repeatedly to mimic a long-running calculation. The loop executes for 20 seconds A DateTime class instance initialized to 20 seconds from the present time controls the loop duration. Listing 1 shows the code as two methods. The method DoSomethingLongThread()uses a separate thread for the loop, wher eas the DoSomethingLongNoThread() method does not.

LISTING 1: Methods to perform a long calculation with and without multithreading.

private void DoSomethingLongNoThread()
   {
     //Get the current date/time.
     DateTime t = DateTime.Now;
     //Add 20 seconds to it.
     t = t.AddSeconds(20);

     //Loop until 20 seconds have elapsed.
     while (t > DateTime.Now);

     MessageBox.Show("Calculations done!");
   }
private void DoSomethingLongThread(object state)
   {
     //Get the current date/time.
     DateTime t = DateTime.Now;
     //Add 20 seconds to it.
     t = t.AddSeconds(20);

     //Loop until 20 seconds have elapsed.
     while (t > DateTime.Now);

     MessageBox.Show("Calculations done!");
     // Signal that the thread is now complete.
     ((AutoResetEvent)state).Set();
     //Enable the buttons.
     button1.Enabled = true;
     button2.Enabled = true;
   }

 

The remainder of the program code consists of the Click() event procedures for the three buttons. Listing 2 contains the code for these methods. When you click Button1, the application uses the techniques discussed in the text to create a new thread using the thread pool.

LISTING 2: Code for the multithreading demonstration program's three buttons.

private void button1_Click(object sender, System.EventArgs e)
  {
     //Disable the buttons.
     button1.Enabled = false;
     button2.Enabled = false;

     //Perform the calculations in a new thread.
     AutoResetEvent asyncOpIsDone = new AutoResetEvent(false);
     ThreadPool.QueueUserWorkItem(new
     WaitCallback(DoSomethingLongThread), asyncOpIsDone);
  }

  private void button2_Click(object sender, System.EventArgs e)
  {
     button1.Enabled = false;
     button2.Enabled = false;

     //Perform the calculations in the program's
     //default thread.
     DoSomethingLongNoThread();
     //Enable the buttons.
     button1.Enabled = true;
     button2.Enabled = true;

  }
  private void button3_Click(object sender, System.EventArgs e)
  {
     //Quit the program.
     this.Close();
  }

 

When you run the program, try it first without multithreading (click the "Start Without Thread" button). You'll find that while the "calculation" process is running, the text box is almost completely unresponsive; you cannot perform any serious editing until the "calculations" are complete. Next, try the "Start With Thread" option. The difference is impossible to miss. You can edit the text box just as if the program were doing nothing else.


When You Need More Control

Using the ThreadPool class for new threads is quite easy, as you have seen; however you may need more control over the threads you create. For example, you may need to assign a priority to a thread, to pause and restart the thread, or to stop it before it has completed. You may also need to synchronize threads, as discussed earlier, to prevent conflicts when accessing resources. When you need this level of control you must use the Thread class. Each instance of the Thread class represents a separate thread. The class members let you set the thread's priority, start, pause, and stop it, and also determine its current state. The details of using the Thread class are beyond the scope of this article, but you can find complete details as well as demonstration programs in the .NET* Framework documentation.

Windows is a preemptive multitasking operating system, giving it the ability to appear to perform multiple tasks simultaneously. The basic unit for multitasking is the thread. A process, or program, typically has only a single thread. In some situations, it can be advantageous for a program to have two or more threads executing at once, a technique called multithreading. In particular, multithreading can greatly improve a program's responsiveness to the user while time-consuming calculations or I/O tasks are being performed. The .NET Framework makes the implementation of multithreading relatively easy, and while multithreading is not a cure-all for performance problems, it is definitely a tool that every programmer should have in his or her toolbox.


About the Author

Peter G. Aitken has been writing about computers and programming for over 10 years, with some 30 books and hundreds of articles to his credit. Recent book titles include Developing Office Solutions With Office 2000 Components and VBA, Windows Script Host, and the soon to be published XML the Microsoft Way. He is a regular contributor to OfficePro magazine, and for several years was a contributing editor for Visual Developer Magazine where he wrote the popular Visual Basic column. Peter is the proprietor of PGA Consulting, providing custom application and Internet development to business, academia, and government since 1994. You can reach him at peter@pgacon.com.






Comments (0)



Leave a comment

Name (required)

Email (required; will not be displayed on this page)

Your URL (optional)


Comment*