12.3 Thread Priorities and Thread Scheduling
Every thread has a priority in the range between ThreadPriority.Lowest to ThreadPriority.Highest. These two values come from the ThreadPriority enumeration (namespace System.Threading). The enumeration consists of the values Lowest, BelowNormal, Normal, AboveNormal and Highest. By default, each thread has priority Normal.
The Windows operating system supports a concept, called timeslicing, that enables threads of equal priority to share a processor. Without timeslicing, each thread in a set of equal-priority threads runs to completion (unless the thread leaves the Running state and enters the WaitSleepJoin, Suspended or Blocked state) before the thread's peers get a chance to execute. With timeslicing, each thread receives a brief burst of processor time, called a quantum, during which the thread can execute. At the completion of the quantum, even if the thread has not finished executing, the processor is taken away from that thread and given to the next thread of equal priority, if one is available.
The job of the thread scheduler is to keep the highest-priority thread running at all times and, if there is more than one highest-priority thread, to ensure that all such threads execute for a quantum in round-robin fashion (i.e., these threads can be timesliced). Figure 12.2 illustrates the multilevel priority queue for threads. In Fig. 12.2, assuming a single-processor computer, threads A and B each execute for a quantum in round-robin fashion until both threads complete execution. This means that A gets a quantum of time to run. Then B gets a quantum. Then A gets another quantum. Then B gets another quantum. This continues until one thread completes. The processor then devotes all its power to the thread that remains (unless another thread of that priority is Started). Next, thread C runs to completion. Threads D, E and F each execute for a quantum in round-robin fashion until they all complete execution. This process continues until all threads run to completion. Note that, depending on the operating system, new higher-priority threads could postponepossibly indefinitelythe execution of lower-priority threads. Such indefinite postponement often is referred to more colorfully as starvation.
Fig. 12.2 Thread-priority scheduling.
A thread's priority can be adjusted with the Priority property, which accepts values from the ThreadPriority enumeration. If the argument is not one of the valid thread-priority constants, an ArgumentException occurs. A thread executes until it dies, becomes Blocked for input/output (or some other reason), calls Sleep, calls Monitor method Wait or Join, is preempted by a thread of higher priority or has its quantum expire. A thread with a higher priority than the Running thread can become Started (and hence preempt the Running thread) if a sleeping thread wakes up, if I/O completes for a thread that Blocked for that I/O, if either Pulse or PulseAll is called on an object on which Wait was called, or if a thread to which the high-priority thread was Joined completes.
Figure 12.3 demonstrates basic threading techniques, including the construction of a Thread object and using the Thread class's static method Sleep. The program creates three threads of execution, each with the default priority Normal. Each thread displays a message indicating that it is going to sleep for a random interval of from 0 to 5000 milliseconds, then goes to sleep. When each thread awakens, the thread displays its name, indicates that it is done sleeping, terminates and enters the Stopped state. You will see that method Main (i.e., the Mainthread of execution) terminates before the application terminates. The program consists of two classesThreadTester (lines 841), which creates the three threads, and MessagePrinter (lines 4473), which defines a Print method containing the actions each thread will perform.
Fig. 12.3 Threads sleeping and printing. (Part 2 of 2)
Objects of class MessagePrinter (lines 4473) control the lifecycle of each of the three threads class ThreadTester's Main method creates. Class MessagePrinter consists of instance variable sleepTime (line 46), static variable random (line 47), a constructor (lines 5054) and a Print method (lines 5771). Variable sleepTime stores a random integer value chosen when a new MessagePrinter object's constructor is called. Each thread controlled by a MessagePrinter object sleeps for the amount of time specified by the corresponding MessagePrinter object's sleepTime
The MessagePrinter constructor (lines 5054) initializes sleepTime to a random integer from 0 up to, but not including, 5001 (i.e., from 0 to 5000).
Method Print begins by obtaining a reference to the currently executing thread (line 60) via class Thread's static property CurrentThread. The currently executing thread is the one that invokes method Print. Next, lines 6364 display a message indicating the name of the currently executing thread and stating that the thread is going to sleep for a certain number of milliseconds. Note that line 64 uses the currently executing thread's Name property to obtain the thread's name (set in method Main when each thread is created). Line 66 invokes staticThread method Sleep to place the thread into the Wait-SleepJoin state. At this point, the thread loses the processor and the system allows another thread to execute. When the thread awakens, it reenters the Started state again until the system assigns a processor to the thread. When the MessagePrinter object enters the Running state again, line 69 outputs the thread's name in a message that indicates the thread is done sleeping, and method Print terminates.
Class ThreadTester's Main method (lines 1039) creates three objects of class MessagePrinter, at lines 14, 19 and 24, respectively. Lines 1516, 2021 and 2526 create and initialize three Thread objects. Lines 17, 22 and 27 set each Thread's Name property, which we use for output purposes. Note that each Thread's constructor receives a ThreadStart delegate as an argument. Remember that a ThreadStart delegate specifies the actions a thread performs during its lifecyle. Line 16 specifies that the delegate for thread1 will be method Print of the object to which printer1 refers. When thread1 enters the Running state for the first time, thread1 will invoke printer1's Print method to perform the tasks specified in method Print's body. Thus, thread1 will print its name, display the amount of time for which it will go to sleep, sleep for that amount of time, wake up and display a message indicating that the thread is done sleeping. At that point method Print will terminate. A thread completes its task when the method specified by a Thread's ThreadStart delegate terminates, placing the thread in the Stopped state. When thread2 and thread3 enter the Running state for the first time, they invoke the Print methods of printer2 and printer3, respectively. Threads thread2 and thread3 perform the same tasks as thread1 by executing the Print methods of the objects to which printer2 and printer3 refer (each of which has its own randomly chosen sleep time).
Testing and Debugging Tip 12.1
Naming threads helps in debugging of a multithreaded program. Visual Studio .NET's displays the name of each thread and enables you to view the execution of any thread in the program. 12.1
Lines 3335 invoke each Thread's Start method to place the threads in the Started state (sometimes called launching a thread). Method Start returns immediately from each invocation, then line 37 outputs a message indicating that the threads were started, and the Main thread of execution terminates. The program itself does not terminate, however, because there are still threads that are alive (i.e., the threads were Started and have not reached the Stopped state yet). The program will not terminate until its last thread dies. When the system assigns a processor to a thread, the thread enters the Running state and calls the method specified by the thread's ThreadStart delegate. In this program, each thread invokes method Print of the appropriate MessagePrinter object to perform the tasks discussed previously.
Note that the sample outputs for this program show each thread and the thread's sleep time as the thread goes to sleep. The thread with the shortest sleep time normally awakens first, indicates that it is done sleeping and terminates.