Tuesday, July 15, 2008

Sample chapter on Multithreading from C#: A Programmer's Introduction

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 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 universities

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, 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,

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 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 Main thread of execution) terminates before the application terminates.

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 Normal

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 Main( string[] args )

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 Main

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 Main when each thread is created).

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: