Get Started with Multithreading in .NET

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 multithreading 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 process. 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 multithreading simply to speed up a program? No, although it can have that effect, particularly on a multiprocessor system. The most important use of multithreading 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 multithreading. 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 thread 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, multithreading introduces another level of programming complexity.

The .NET Framework provides two methods to 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:

  1. 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).
  2. 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 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.

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.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles:

About

DevX is the leading provider of technical information, tools, and services for professionals developing corporate applications. 

Subscribe

Get exclusive access to the best technical information, tools, and services sent straight to your inbox.  

©2023 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.