Browse DevX
Sign up for e-mail newsletters from DevX


Working with .NET Threads : Page 6

This article describes the dos and don'ts of the Thread class, and presents a wrapper class that simplifies starting a thread, correctly terminates a thread, and offers a more consistent class interface than that of the raw Thread class.

Thread Priority and Scheduling
Each thread is allocated a fixed time slot to run on the CPU, and it is assigned a priority. In addition, a thread is either ready to run, or it is waiting for some event to occur, such as a synchronization object being signaled or a sleep timeout to elapse. The underlying operating system schedules for execution those threads that are ready to run based on the thread's priority. Thread scheduling is preemptive, meaning that the thread with the highest priority always gets to run. If a thread T1 with priority P1 is running, and suddenly thread T2 with priority P2 is ready to run, and P2 is greater than P1, the operating system will preempt (pause) T1 and allow T2 to run. If multiple threads with the highest priority are ready to run, the operating system will let each run its CPU time slot and then preempt it in favor of another thread with the same priority, in a round-robin fashion.

The Thread class provides the Priority property of the enumeration type ThreadPriority, which allows you to retrieve or set the thread priority:

   public ThreadPriority Priority { get; set; }
The enum ThreadPriority provides five priority levels:

   public enum ThreadPriority 
New .NET threads are created by default with a priority of ThreadPriority.Normal. Developers often abuse thread priority settings as a means to control the flow of a multithreaded application to work around race conditions. Tinkering with thread priorities generally is not an appropriate solution and can lead to some adverse side effects and other race conditions. For example, imagine two threads that are involved in a race condition. By increasing one thread's priority, in the hope that it will run at the expense of the other and win the race, you often just decrease the probability of the race condition, because the thread with the higher priority can still be switched out or perform blocking operations. In addition, does it make sense to always run that thread at a higher priority? That could paralyze other aspects of your application. You could, of course, increase the priority only temporarily, but then you would address just that particular occurrence of the race condition, and remain exposed to future occurrences.

You may be tempted to always keep that thread at a high priority, and increase the priority of other affected threads. Often, increasing one thread's priority causes an inflation of increased thread priorities all around, because the normal balance and time-sharing governed by the operating system is disturbed. The result can be a set of threads, all with the highest priority, still involved with race conditions. The major adverse effect now is that .NET suffers, because many of its internal threads (such as threads used to manage memory, execute remote calls, and so on) are suddenly competing with your threads. In addition, preemptive operating systems (like Windows) will dynamically change threads' priorities to resolve priority inversions situations.

A priority inversion occurs when threads with lower priority run instead of threads with a higher priority. Because .NET threads are currently mapped to the underlying Windows threads, these dynamic changes propagate to the managed threads as well. Consider for example three managed threads T1, T2, T3, with respective priorities of ThreadPriority.Lowest, ThreadPriority.Normal, and ThreadPriority.Highest. T3 is waiting for a resource held by T1. T1 is ready to run to release the resource, except that T2 is now running, always preventing T1 from executing. As a result, T2 prevents T3 from running, and priority inversion takes place, because T3 has a priority greater than that of T2.

To cope with priority inversions, the operating system not only keeps track of thread priorities, but also maintains a scoreboard showing who got to run and how often. If a thread is denied the CPU for a long time (a few seconds), the operating system dynamically boosts that thread's priority to a higher priority, letting it run for a couple of time slots with the new priority, and then sets the priority back to its original value. In the previous scenario, this allows T1 to run, release the resource T3 is waiting for, and then regain its original priority. T3 will be ready to run (because the resource is now available) and will preempt T2. The point of this example and the other arguments is that you should avoid controlling the application flow by setting thread priorities. Use .NET synchronization objects to control and coordinate the flow of your application and to resolve race conditions. Set threads priorities to values other than normal only when the semantics of the application requires it. For example, if you develop a screen saver, its threads should run at priority ThreadPriority.Lowest so that other background operations such as compilation, network access, or number crunching could take place, and not be affected by the screen saver.

Thanks for your registration, follow us on our social networks to keep up-to-date