12.1 Introduction
It would be nice if we could perform one action at a time and perform it well, but that is
usually difficult to do. The human body performs a great variety of operations in parallel—
or, as we will say throughout this chapter, concurrently. Respiration, blood circulation and
digestion, for example, can occur concurrently. All the senses—sight, touch, smell, taste
and hearing—can occur at once. Computers, too, perform operations concurrently. It is
common for desktop personal computers to be compiling a program, sending a file to a
printer and receiving electronic mail messages over a network concurrently.
Ironically, most programming languages do not enable programmers to specify concurrent
activities. Rather, programming languages generally provide only a simple set of
control structures that enable programmers to perform one action at a time, proceeding to
the next action after the previous one has finished. Historically, the type of concurrency that
computers perform today generally has been implemented as operating system “primitives”
available only to highly experienced “systems programmers.”
The
Defense, made concurrency primitives widely available to defense contractors building
military command-and-control systems. However,
and commercial industry.
The .NET Framework Class Library makes concurrency primitives available to the
applications programmer. The programmer specifies that applications contain “threads of
execution,” each thread designating a portion of a program that may execute concurrently
with other threads—this capability is called multithreading. Multithreading is available to
all .NET programming languages, including C#, Visual Basic and Visual C++.
Software Engineering Observation 12.1
The .NET Framework Class Library includes multithreading capabilities in namespace
System.Threading. This encourages the use of multithreading among a larger part of
the applications-programming community. 12.1
We discuss many applications of concurrent programming. When programs download
large files, such as audio clips or video clips from the World Wide Web, users do not want
to wait until an entire clip downloads before starting the playback. To solve this problem,
we can put multiple threads to work—one thread downloads a clip, and another plays the
clip. These activities, or tasks, then may proceed concurrently. To avoid choppy playback,
we synchronize the threads so that the player thread does not begin until there is a sufficient
amount of the clip in memory to keep the player thread busy.
Another example of multithreading is C#’s automatic garbage collection. C and C++
place with the programmer the responsibility of reclaiming dynamically allocated memory.
Outline
12.1 Introduction
12.2 Thread States: Life Cycle of a Thread
12.3 Thread Priorities and Thread Scheduling
12.4 Summary
Chapter 12 Multithreading 409
C# provides a garbage-collector thread that reclaims dynamically allocated memory that
is no longer needed.
Performance Tip 12.1
One of the reasons for the popularity of C and C++ over the years was that their memorymanagement
techniques were more efficient than those of languages that used g*arbage collectors.
In fact, memory management in C# often is faster than in C or C++.1
12.1
Good Programming Practice 12.1
Set an object reference to null when the program no longer needs that object. This enables
the garbage collector to determine at the earliest possible moment that the object can be garbage
collected. If such an object has other references to it, that object cannot be collected.12.1
Writing multithreaded programs can be tricky. Although the human mind can perform
functions concurrently, people find it difficult to jump between parallel “trains of thought.”
To see why multithreading can be difficult to program and understand, try the following
experiment: Open three books to page 1 and try reading the books concurrently. Read a few
words from the first book, then read a few words from the second book, then read a few
words from the third book, then loop back and read the next few words from the first book,
etc. After this experiment, you will appreciate the challenges of multithreading—switching
between books, reading briefly, remembering your place in each book, moving the book
you are reading closer so you can see it, pushing books you are not reading aside—and
amidst all this chaos, trying to comprehend the content of the books!
Performance Tip 12.2
A problem with single-threaded applications is that lengthy activities must complete before
other activities can begin. In a multithreaded application, threads can share a processor (or
set of processors), so that multiple tasks are performed in parallel. 12.2
12.2 Thread States: Life Cycle of a Thread
At any time, a thread is said to be in one of several thread states (illustrated in Fig. 12.1)2.
This section discusses these states and the transitions between states. Two classes critical
for multithreaded applications are Thread and Monitor (System.Threading
namespace). This section also discusses several methods of classes Thread and Monitor
that cause state transitions.
A new thread begins its lifecyle in the Unstarted state. The thread remains in the
Unstarted state until the program calls Thread method Start, which places the thread
in the Started state (sometimes called the Ready or Runnable state) and immediately returns
control to the calling thread. Then the thread that invoked Start, the newly Started thread
and any other threads in the program execute concurrently.
1. E. Schanzer, “Performance Considerations for Run-Time Technologies in the .NET Framework,”
August 2001
/library/en-us/dndotnet/html/dotnetperftechs.asp>.
2. As this book went to publication, Microsoft changed the names of the Started and Blocked thread
states to Running and WaitSleepJoin, respectively.
410 Multithreading Chapter 12
The highest priority Started thread enters the Running state (i.e., begins executing)
when the operating system assigns a processor to the thread (Section 12.3 discusses thread
priorities). When a Started thread receives a processor for the first time and becomes a Running
thread, the thread executes its ThreadStart delegate, which specifies the actions
the thread will perform during its lifecyle. When a program creates a new Thread, the program
specifies the Thread’s ThreadStart delegate as the argument to the Thread
constructor. The ThreadStart delegate must be a method that returns void and takes
no arguments.
A Running thread enters the Stopped (or Dead) state when its ThreadStart delegate
terminates. Note that a program can force a thread into the Stopped state by calling
Thread method Abort on the appropriate Thread object. Method Abort throws a
ThreadAbortException in the thread, normally causing the thread to terminate.
When a thread is in the Stopped state and there are no references to the thread object, the
garbage collector can remove the thread object from memory.
A thread enters the Blocked state when the thread issues an input/output request. The
operating system blocks the thread from executing until the operating system can complete
the I/O for which the thread is waiting. At that point, the thread returns to the Started state, so
it can resume execution. A Blocked thread cannot use a processor even if one is available.
Fig. 12.1 Thread life cycle.
Started
Running
WaitSleepJoin Suspended Stopped Blocked
Unstarted
dispatch
(assign a
processor)
quantum
expiration
Start
I/O completion
Suspend Issue I/O request
Wait
Interrupt
sleep interval expires
Resume
Sleep, Join
Pulse
PulseAll
complete
Chapter 12 Multithreading 411
There are three ways in which a Running thread enters the WaitSleepJoin state. If a
thread encounters code that it cannot execute yet (normally because a condition is not satisfied),
the thread can call Monitor method Wait to enter the WaitSleepJoin state.
Once in this state, a thread returns to the Started state when another thread invokes Monitor
method Pulse or PulseAll. Method Pulse moves the next waiting thread
back to the Started state. Method PulseAll moves all waiting threads back to the
Started state.
A Running thread can call Thread method Sleep to enter the WaitSleepJoin state
for a period of milliseconds specified as the argument to Sleep. A sleeping thread returns
to the Started state when its designated sleep time expires. Sleeping threads cannot use a
processor, even if one is available.
Any thread that enters the WaitSleepJoin state by calling Monitor method Wait or
by calling Thread method Sleep also leaves the WaitSleepJoin state and returns to the
Started state if the sleeping or waiting Thread’s Interrupt method is called by another
thread in the program.
If a thread cannot continue executing (we will call this the dependent thread) unless
another thread terminates, the dependent thread calls the other thread’s Join method to
“join” the two threads. When two threads are “joined,” the dependent thread leaves the
WaitSleepJoin state when the other thread finishes execution (enters the Stopped state).
If a Running Thread’s Suspend method is called, the Running thread enters the Suspended
state. A Suspended thread returns to the Started state when another thread in the
program invokes the Suspended thread’s Resume method.
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,
thread has priority
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,
412 Multithreading Chapter 12
thread C runs to completion. Threads D, E and F each execute for a quantum in roundrobin
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 postpone—possibly indefinitely—the execution of lower-priority threads.
Such indefinite postponement often is referred to more colorfully as starvation.
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
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
The program consists of two classes—ThreadTester (lines 8–41), which creates
the three threads, and MessagePrinter (lines 44–73), which defines a Print method
containing the actions each thread will perform.
Fig. 12.2 Thread-priority scheduling.
Priority Highest
Priority AboveNormal
Priority
Priority BelowNormal
Priority Lowest
Ready threads
A B
C
D E F
G
Chapter 12 Multithreading 413
1 // Fig. 12.3: ThreadTester.cs
2 // Multiple threads printing at different intervals.
34
using System;
5 using System.Threading;
67
// class ThreadTester demonstrates basic threading concepts
8 class ThreadTester
9 {
10 static void
11 {
12 // Create and name each thread. Use MessagePrinter's
13 // Print method as argument to ThreadStart delegate.
14 MessagePrinter printer1 = new MessagePrinter();
15 Thread thread1 =
16 new Thread ( new ThreadStart( printer1.Print ) );
17 thread1.Name = "thread1";
18
19 MessagePrinter printer2 = new MessagePrinter();
20 Thread thread2 =
21 new Thread ( new ThreadStart( printer2.Print ) );
22 thread2.Name = "thread2";
23
24 MessagePrinter printer3 = new MessagePrinter();
25 Thread thread3 =
26 new Thread ( new ThreadStart( printer3.Print ) );
27 thread3.Name = "thread3";
28
29 Console.WriteLine( "Starting threads" );
30
31 // call each thread's Start method to place each
32 // thread in Started state
33 thread1.Start();
34 thread2.Start();
35 thread3.Start();
36
37 Console.WriteLine( "Threads started\n" );
38
39 } // end method
40 } // end class ThreadTester
41
42 // Print method of this class used to control threads
43 class MessagePrinter
44 {
45 private int sleepTime;
46 private static Random random = new Random();
47
48 // constructor to initialize a MessagePrinter object
49 public MessagePrinter()
50 {
51 // pick random sleep time between 0 and 5 seconds
52 sleepTime = random.Next( 5001 );
53 }
Fig. 12.3 Threads sleeping and printing. (Part 1 of 2)
414 Multithreading Chapter 12
Objects of class MessagePrinter (lines 44–73) 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 50–54) and a Print method (lines 57–71). 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
54
55 // method Print controls thread that prints messages
56 public void Print()
57 {
58 // obtain reference to currently executing thread
59 Thread current = Thread.CurrentThread;
60
61 // put thread to sleep for sleepTime amount of time
62 Console.WriteLine(
63 current.Name + " going to sleep for " + sleepTime );
64
65 Thread.Sleep ( sleepTime );
66
67 // print thread name
68 Console.WriteLine( current.Name + " done sleeping" );
69
70 } // end method Print
71
72 } // end class MessagePrinter
Starting threads
Threads started
thread1 going to sleep for 1977
thread2 going to sleep for 4513
thread3 going to sleep for 1261
thread3 done sleeping
thread1 done sleeping
thread2 done sleeping
Starting threads
Threads started
thread1 going to sleep for 1466
thread2 going to sleep for 4245
thread3 going to sleep for 1929
thread1 done sleeping
thread3 done sleeping
thread2 done sleeping
Fig. 12.3 Threads sleeping and printing. (Part 2 of 2)
Chapter 12 Multithreading 415
The MessagePrinter constructor (lines 50–54) 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 63–64 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
Line 66 invokes static Thread 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 10–39) creates three objects of class
MessagePrinter, at lines 14, 19 and 24, respectively. Lines 15–16, 20–21 and 25–26
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 the debugging of a multithreaded program. Visual Studio .NET’s
debugger provides a Threads window that displays the name of each thread and enables
you to view the execution of any thread in the program. 12.1
Lines 33–35 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.
416 Multithreading Chapter 12
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.
12.4 Summary
Computers perform multiple operations concurrently. Programming languages generally
provide only a simple set of control structures that enable programmers to perform just one
action at a time and proceed to the next action only after the previous one finishes. The
FCL, however, provides the C# programmer with the ability to specify that applications
contain threads of execution, where each thread designates a portion of a program that may
execute concurrently with other threads. This capability is called multithreading.
A thread is initialized using the Thread class’s constructor, which receives a
ThreadStart delegate. This delegate specifies the method that contains the tasks a
thread will perform. A thread remains in the Unstarted state until the thread’s Start
method is called, which the thread enters the Started state. A thread in the Started state
enters the Running state when the system assigns a processor to the thread. The system
assigns the processor to the highest-priority Started thread. A thread enters the Stopped
state when its ThreadStart delegate completes or terminates. A thread is forced into the
Stopped state when its Abort method is called (by itself or by another thread). A Running
thread enters the Blocked state when the thread issues an input/output request. A Blocked
thread becomes Started when the I/O it is waiting for completes. A Blocked thread cannot
use a processor, even if one is available.
If a thread needs to sleep, it calls method Sleep. A thread wakes up when the designated
sleep interval expires. If a thread cannot continue executing unless another thread terminates,
the first thread, referred to as the dependent thread, calls the other thread’s Join
method to “join” the two threads. When two threads are joined, the dependent thread leaves
the WaitSleepJoin state when the other thread finishes execution.
When a thread encounters code that it cannot yet run, the thread can call Monitor
method Wait until certain actions occur that enable the thread to continue executing. This
method call puts the thread into the WaitSleepJoin state. Any thread in the WaitSleepJoin
state can leave that state if another thread invokes Thread method Interrupt on the
thread in the WaitSleepJoin state. If a thread has called Monitor method Wait, a corresponding
call to Monitor method Pulse or PulseAll by another thread in the program
will transition the original thread from the WaitSleepJoin state to the Started state.
If Thread method Suspend is called on a thread, the thread enters the Suspended
state. A thread leaves the Suspended state when a separate thread invokes Thread method
Resume on the suspended thread.
Every C# thread has a priority. The job of the thread scheduler is to keep the highestpriority
thread running at all times and, if there is more than one highest-priority thread, to
ensure that all equally high-priority threads execute for a quantum at a time in round-robin
fashion. A thread’s priority can be adjusted with the Priority property, which is
assigned an argument from the ThreadPriority enumeration.
No comments:
Post a Comment