csphtp1.book Page 590 Wednesday, November 21, 2001 11:59 AM
14
Multithreading
Objectives
To understand the notion of multithreading.
To appreciate how multithreading can improve
program performance.
To understand how to create, manage and destroy
threads.
To understand the life cycle of a thread.
To understand thread synchronization.
To understand thread priorities and scheduling.
To understand the role of a ThreadPool in efficient
multithreading.
The spider’s touch, how exquisitely fine!
Feels at each thread, and lives along the line.
Alexander Pope
A person with one watch knows what time it is; a person with
two watches is never sure.
Proverb
Learn to labor and to wait.
Henry Wadsworth Longfellow
The most general definition of beauty…Multeity in Unity.
Samuel Taylor Coleridge
csphtp1.book Page 591 Wednesday, November 21, 2001 11:59 AM
Chapter 14
Multithreading
591
Outline
14.1
14.2
14.3
14.4
14.5
14.6
14.7
Introduction
Thread States: Life Cycle of a Thread
Thread Priorities and Thread Scheduling
Thread Synchronization and Class Monitor
Producer/Consumer Relationship without Thread Synchronization
Producer/Consumer Relationship with Thread Synchronization
Producer/Consumer Relationship: Circular Buffer
Summary Terminology Self-Review Exercises Answers to Self-Review Exercises Exercises
14.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 con-
current 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 Ada programming language, developed by the United States Department of
Defense, made concurrency primitives widely available to defense contractors building
military command-and-control systems. However, Ada has not been widely used in univer-
sities 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 14.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.
14.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
csphtp1.book Page 592 Wednesday, November 21, 2001 11:59 AM
592
Multithreading
Chapter 14
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.
C# provides a garbage-collector thread that reclaims dynamically allocated memory that
is no longer needed.
Performance Tip 14.1
One of the reasons for the popularity of C and C++ over the years was that their memory-
management techniques were more efficient than those of languages that used garbage col-
lectors. In fact, memory management in C# often is faster than in C or C++.1
14.1
Good Programming Practice 14.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 gar-
bage collected. If such an object has other references to it, that object cannot be collected. 14.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 14.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.
14.2
14.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. 14.12).
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 Moni-
tor that cause state transitions.
1. E. Schanzer, “Performance Considerations for Run-Time Technologies in the .NET Framework,”
August 2001 .
2. As this book went to publication, Microsoft changed the names of the Started and Blocked thread
states to Running and WaitSleepJoin, respectively.
csphtp1.book Page 593 Wednesday, November 21, 2001 11:59 AM
Chapter 14
Multithreading
593
Unstarted
Start
Pulse
PulseAll
Interrupt
sleep interval expires
Started
quantum
expiration
dispatch
(assign a
processor)
Running
I/O completion
Wait
Sleep, Join
Suspend
complete
Issue I/O request
WaitSleepJoin
Suspended
Stopped
Blocked
Resume
Fig. 14.1
Fig. 14.1
Fig. 14.1
Fig. 14.1
Thread life cycle.
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.
The highest priority Started thread enters the Running state (i.e., begins executing)
when the operating system assigns a processor to the thread (Section 14.3 discusses thread
priorities). When a Started thread receives a processor for the first time and becomes a Run-
ning 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 pro-
gram 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 dele-
gate 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.
csphtp1.book Page 594 Wednesday, November 21, 2001 11:59 AM
594
Multithreading
Chapter 14
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.
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 sat-
isfied), 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 Sus-
pended state. A Suspended thread returns to the Started state when another thread in the
program invokes the Suspended thread’s Resume method.
14.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. Figure 14.2 illustrates the multilevel priority queue for
threads. In Fig. 14.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
csphtp1.book Page 595 Wednesday, November 21, 2001 11:59 AM
Chapter 14
Multithreading
595
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 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 14.3 demonstrates basic threading techniques, including the construction of a
Thread object and using the Thread class’s static method Sleep. The program cre-
ates three threads of execution, each with the default priority Normal. Each thread dis-
plays 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 Main thread of execution) terminates before the application termi-
nates. 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.
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
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 indi-
cating 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 cre-
ated). 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
csphtp1.book Page 596 Wednesday, November 21, 2001 11:59 AM
596
Multithreading
Chapter 14
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).
Priority Highest
Priority AboveNormal
Priority Normal
Priority BelowNormal
Priority Lowest
Ready threads
B
E
F
A
C
D
G
Fig. 14.2
Fig. 14.2
Fig. 14.2
Fig. 14.2
Thread-priority scheduling.
// Fig. 14.3: ThreadTester.cs
// Multiple threads printing at different intervals.
1
2
3
Fig. 14.3
Fig. 14.3
Fig. 14.3
Fig. 14.3
Threads sleeping and printing. (Part 1 of 3.)
csphtp1.book Page 597 Wednesday, November 21, 2001 11:59 AM
Chapter 14
Multithreading
597
using System;
using System.Threading;
// class ThreadTester demonstrates basic threading concepts
class ThreadTester
{
static void Main( string[] args )
{
// Create and name each thread. Use MessagePrinter's
// Print method as argument to ThreadStart delegate.
MessagePrinter printer1 = new MessagePrinter();
Thread thread1 =
new Thread ( new ThreadStart( printer1.Print ) );
thread1.Name = "thread1";
MessagePrinter printer2 = new MessagePrinter();
Thread thread2 =
new Thread ( new ThreadStart( printer2.Print ) );
thread2.Name = "thread2";
MessagePrinter printer3 = new MessagePrinter();
Thread thread3 =
new Thread ( new ThreadStart( printer3.Print ) );
thread3.Name = "thread3";
Console.WriteLine( "Starting threads" );
// call each thread's Start method to place each
// thread in Started state
thread1.Start();
thread2.Start();
thread3.Start();
Console.WriteLine( "Threads started\n" );
} // end method Main
} // end class ThreadTester
// Print method of this class used to control threads
class MessagePrinter
{
private int sleepTime;
private static Random random = new Random();
// constructor to initialize a MessagePrinter object
public MessagePrinter()
{
// pick random sleep time between 0 and 5 seconds
sleepTime = random.Next( 5001 );
}
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Fig. 14.3
Fig. 14.3
Fig. 14.3
Fig. 14.3
Threads sleeping and printing. (Part 2 of 3.)