Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

modern-multithreading-c-java

.pdf
Скачиваний:
18
Добавлен:
22.05.2015
Размер:
4.77 Mб
Скачать

EXERCISES

45

žT(P): total running time of the program, including both the serial and parallel parts using P processors, which equals ts + tp(P) = ts + [tp(1)/P]. From Amdahl’s law, we have

speedup = T(1)/T(P) = [ts + tp(1)]/{ts + [tp(1)/P]}.

Let P = 10, tp(P) = 4, and ts = 6. What is the speedup?

2

THE CRITICAL SECTION PROBLEM

As an introduction to concurrent programming, we study a fundamental problem called the critical section problem [Dijkstra 1965]. The problem is easy to understand and its solutions are small in terms of the number of statements they contain (usually, fewer than five). However, the critical section problem is not easy to solve, and it illustrates just how difficult it can be to write even small concurrent programs.

A code segment that accesses shared variables (or other shared resources) and that has to be executed as an atomic action is referred to as a critical section. The critical section problem involves a number of threads that are each executing the following code:

while (true) {

 

entry-section

 

critical section

// accesses shared variables or other shared resources.

exit-section

 

noncritical section

// a thread may terminate its execution in this section.

}

 

The entryand exit-sections that surround a critical section must satisfy the following correctness requirements [Silberschatz et al. 1991]:

Modern Multithreading: Implementing, Testing, and Debugging Multithreaded Java and C++/Pthreads/Win32 Programs, By Richard H. Carver and Kuo-Chung Tai Copyright 2006 John Wiley & Sons, Inc.

46

SOFTWARE SOLUTIONS TO THE TWO-THREAD CRITICAL SECTION PROBLEM

47

žMutual exclusion. When a thread is executing in its critical section, no other threads can be executing in their critical sections. (If a thread is executing in its critical section when a context switch occurs, the thread is still considered to be in the critical section. With the assumption of fair scheduling, this thread will eventually resume execution and exit the critical section.)

žProgress. If no thread is executing in its critical section and there are threads that wish to enter their critical sections, only the threads that are executing in their entryor exit-sections can participate in the decision about which thread will enter its critical section next, and this decision cannot be postponed indefinitely.

žBounded waiting. After a thread makes a request to enter its critical section, there is a bound on the number of times that other threads are allowed to enter their critical sections before this thread’s request is granted.

We assume that a thread that enters its critical section will eventually exit its critical section. (If a thread is stuck in an infinite loop inside its critical section, or the thread terminates in its critical section, other threads waiting to enter their critical sections will wait forever.) The noncritical section may contain a statement that terminates a thread’s execution.

If the threads are being time-sliced on a single processor, one simple solution to the critical section problem is for each thread to disable interrupts before it enters its critical section:

disableInterrupts();

// disable all interrupts

critical section

 

enableInterrupts();

// enable all interrupts

This disables interrupts from the interval timer that is used for time slicing and prevents any other thread from running until interrupts are enabled again. However, commands for disabling and enabling interrupts are not always available to user code. Also, this solution does not work on systems with multiple CPUs when a thread on one processor is not able to disable interrupts on another processor.

In this chapter we present low-level software and hardware solutions to the critical section problem. In Chapters 3 and 4 we show a number of solutions that use high-level synchronization constructs.

2.1 SOFTWARE SOLUTIONS TO THE TWO-THREAD CRITICAL SECTION PROBLEM

We first consider the case where there are only two threads T0 and T1 that are attempting to enter their critical sections. Threads T0 and T1 may be executing in their entry-sections at the same time. Informally, the entry-section of thread T0

48

THE CRITICAL SECTION PROBLEM

should enforce the following rules (thread T1’s entryand exit-sections mirror those of T0):

žIf thread T1 is in its critical section, thread T0 must wait in its entry-section.

žIf thread T1 is not its critical section and does not want to enter its critical section, thread T0 should be allowed to enter its critical section.

žWhen both threads want to enter their critical sections, only one of them should be allowed to enter.

The exit-section of thread T0 should do the following:

ž If thread T1 is waiting to enter its critical sections, allow T1 to enter.

The derivation of correct entryand exit-sections is not a trivial problem. It is difficult to find a solution that satisfies all three correctness properties. The solutions we examine do not use any special programming constructs; they use global variables, arrays, loops, and if-statements, all of which are found in sequential programming languages. This allows us to study a difficult concurrent programming problem without introducing new programming language constructs.

Although shared variables are used in the entryand exit-sections, this will not create another mutual exclusion problem. The entryand exit-sections will be written carefully so that all assignment statements and expressions involving shared variables are atomic operations and there are no groups of statements that must be executed atomically. Thus, the entryand exit-sections themselves need not be critical sections.

The solutions below assume that the hardware provides mutual exclusion for individual read and write operations on shared variables. This means that the memory system must implement its own solution to the critical section problem. (Later, we will see a solution that does not require atomic reads and writes of variables.) Our focus here is on condition synchronization (i.e., defining the conditions under which threads can enter their critical sections). As we will see in later chapters, high-level synchronization constructs make it easy to create critical sections, but condition synchronization will always remain difficult to achieve.

We first present a number of incorrect solutions to the critical section problem. Then we show a correct solution.

2.1.1 Incorrect Solution 1

Threads T0 and T1 use variables intendToEnter0 and intendToEnter1 to indicate their intention to enter their critical section. A thread will not enter its critical section if the other thread has already signaled its intention to enter.

SOFTWARE SOLUTIONS TO THE TWO-THREAD CRITICAL SECTION PROBLEM

49

boolean intendToEnter0=false, intendToEnter1=false;

 

 

 

T0

 

T1

 

 

 

 

 

 

 

 

 

while (true) {

 

while (true) {

 

 

 

while (intendToEnter1) {;}

(1)

while (intendToEnter0) {;}

(1)

 

 

intendToEnter0 = true;

(2)

intendToEnter1 = true;

(2)

 

 

critical section

(3)

critical section

(3)

 

intendToEnter0 = false;

(4)

intendToEnter1 = false;

(4)

 

noncritical section

(5)

noncritical section

(5)

 

}

 

}

 

 

This solution does not guarantee mutual exclusion. The following execution sequence shows a possible interleaving of the statements in T0 and T1 that ends with both T0 and T1 in their critical sections.

T0

T1

 

Comments

 

 

 

 

(1)

context switch →

T0

exits its while-loop

 

 

 

 

(1) T1

exits its while-loop

 

(2)

intendToEnter1 is set to true

 

(3)

T1

enters its critical section

(2)

← context switch

intendToEnter0 is set to true

 

(3)

 

T0 enters its critical section

2.1.2 Incorrect Solution 2

Global variable turn is used to indicate which thread is allowed to enter its critical section (i.e., the threads take turns entering their critical sections). The initial value of turn can be 0 or 1.

int turn = 1;

 

 

 

 

T0

 

T1

 

 

 

 

 

 

 

while (true) {

 

while (true) {

 

 

while (turn !=0){;}

(1)

while (turn !=1){;}

(1)

 

critical section

(2)

critical section

(2)

 

turn = 1;

(3)

turn = 0;

(3)

 

noncritical section

(4)

noncritical section

(4)

}

 

}

 

50

THE CRITICAL SECTION PROBLEM

This solution forces T0 and T1 to alternate their entries into the critical section and thus ensures mutual exclusion and bounded waiting. Assume that the initial value of turn is 1 and consider the following execution sequence:

T0

T1

Comments

(1)T1 exits its while-loop

(2)T1 enters and exits its critical section

(3)turn is set to 0

(4)T1 terminates in its noncritical section

(1)

← context switch

exits its while-loop

T0

(2)

T0

enters and exits its critical section

(3)

turn is set to 1

(4)

T0

executes its noncritical section

(1)

T0

repeats (1) forever

Thread T0 cannot exit the loop in (1) since the value of turn is 1 and turn will never be changed by T1. Thus, the progress requirement is violated.

2.1.3 Incorrect Solution 3

Solution 3 is a more “polite” version of Solution 1. When one thread finds that the other thread also intends to enter its critical section, it sets its own intendToEnter flag to false and waits until the other thread exits its critical section.

T0

 

T1

 

 

 

 

 

while (true) {

 

while (true) {

 

intendToEnter0 = true;

(1)

intendToEnter1 = true;

(1)

while (intendToEnter1) {

(2)

while (intendToEnter0) {

(2)

intendToEnter0 = false;

(3)

intendToEnter1 = false;

(3)

while(intendToEnter1) {;}

(4)

while(intendToEnter0) {;}

(4)

intendToEnter0 = true;

(5)

intendToEnter1 = true;

(5)

}

 

}

 

critical section

(6)

critical section

(6)

intendToEnter0 = false;

(7)

intendToEnter1 = false;

(7)

noncritical section

(8)

noncritical section

(8)

}

 

}

 

This solution ensures that when both intendToEnter0 and intendToEnter1 are true, only one of T0 and T1 is allowed to enter its critical section. Thus, the mutual exclusion requirement is satisfied. However, there is a problem with this solution, as illustrated by the following execution sequence:

SOFTWARE SOLUTIONS TO THE TWO-THREAD CRITICAL SECTION PROBLEM

51

T0

T1

Comments

 

 

 

 

(1)

intendToEnter0 is set to true

 

(2)

T0

exits the first while-loop in the entry

 

section

 

(6)

T0

enters its critical section;

 

 

intendToEnter0 is true

 

 

context switch →

 

 

(1)intendToEnter1 is set to true

(2)–(3) intendToEnter1 is set to false

(4)T1 enters the second while-loop in the

← context switch

entry section

 

(7)

intendToEnter0 is set to false

(8)

T0 executes its noncritical section

(1)

intendToEnter0 is set to true

(2)

T0 exits the first while-loop in the entry

 

section

(6)

T0 enters its critical section;

context switch →

intendToEnter0 is true

 

(4)

T1 is still waiting for intendToEnter0 to

← context switch

be false

 

(7)

 

. . . repeat infinitely

 

In this execution sequence, T0 enters its critical section infinitely often and T1 waits forever to enter its critical section. Thus, this solution does not guarantee bounded waiting.

The foregoing incorrect solutions to the critical section problem raise the following question: How can we determine whether a solution to the critical section problem is correct? One approach is to use mathematical proofs (see [Andrews 1991]). Another formal approach is to generate all the possible interleavings of the atomic actions in the solution and then check that all the interleavings satisfy the three required correctness properties (see Chapter 7). Next, we suggest an informal approach to checking the correctness of a solution.

When checking a solution to the critical section problem, consider each of these three important cases:

1.Thread T0 intends to enter its critical section and thread T1 is not in its critical section or in its entry-section. In this case, if T0 cannot enter its critical section, the progress requirement is violated.

2.Thread T0 intends to enter its critical section and thread T1 is in its critical section. In this case, if both threads can be in their critical sections, the mutual exclusion requirement is violated.

52

THE CRITICAL SECTION PROBLEM

3.Both threads intend to enter their critical sections (i.e., both threads are in their entry-sections, or one is in its entry-section and one is in its exitsection). This case is difficult to analyze since we have to consider all possible interleavings of statements in the entryand exit-sections in order to detect violations of the mutual exclusion, progress, and bounded waiting requirements.

Next we show a correct solution to the critical section problem and apply our informal correctness check. We invite the reader to try to solve the critical section problem before looking at the solution!

2.1.4 Peterson’s Algorithm

Peterson’s algorithm is a combination of solutions 2 and 3. If both threads intend to enter their critical sections, turn is used to break the tie.

boolean intendToEnter0 = false, intendToEnter1 = false; int turn; // no initial value for turn is needed.

T0

 

T1

 

 

 

 

 

while (true) {

 

while (true) {

 

intendToEnter0 = true;

(1)

intendToEnter1 = true;

(1)

turn = 1;

(2)

turn = 0;

(2)

while (intendToEnter1 &&

(3)

while (intendToEnter0 &&

(3)

turn == 1) {;}

 

turn == 0) {;}

 

critical section

(4)

critical section

(4)

intendToEnter0 = false;

(5)

intendToEnter1 = false;

(5)

noncritical section

(6)

noncritical section

(6)

}

 

}

 

A formal proof of Peterson’s algorithm is given in [Silberschatz et al. 1991, p. 161]. Here, we show only that the algorithm works in each of three important cases:

1.Assume that thread T0 intends to enter its critical section and T1 is not in its critical section or its entry-section. Then intendToEnter 0 is true and intendToEnter 1 is false so T0 can enter its critical section.

2.Assume that thread T0 intends to enter its critical section and T1 is in its critical section. Since turn = 1, T0 loops at statement (3). After the execution of (5) by T1, if T0 resumes execution before T1 tries to enter again, then T0 can enter its critical section; otherwise, see case (3).

3.Assume that both threads intend to enter the critical section (i.e., both threads have set their intendToEnter flags to true). The first thread that executes "t urn = . . .;" waits until the other thread executes "turn = . . .;" and then the first thread enters its critical section. The second thread will

SOFTWARE SOLUTIONS TO THE TWO-THREAD CRITICAL SECTION PROBLEM

53

enter after the first thread exits. This case is illustrated by the following execution sequence:

T0

T1

 

Comments

 

 

 

(1)

context switch →

intendToEnter0 set to true

 

 

 

 

(1)

intendToEnter1 is set to true

 

(2) turn is set to 0

 

(3) T1

enters its while-loop

(2)

← context switch

turn is set to 1

 

(3)

context switch →

T0

enters its while-loop

 

 

 

 

(3) T1

exits its while-loop

 

(4)

T1

enters and exits its critical section

 

(5)

intendToEnter1 is set to false

 

(6)

T1

executes its noncritical section

 

(1)

intendToEnter1 is set to true

 

(2) turn is set to 0

 

(3) T1

enters its while-loop

(3)

← context switch

T0

exits its while-loop

 

(4)

 

T0

enters and exits its critical section

(5)

 

intendToEnter0 is set to false

(6)

context switch →

T0

executes its noncritical section

 

 

 

 

(3) T1

exits its while-loop

 

(4)

T1

enters its critical section

Peterson’s algorithm is often called the tie-breaker algorithm, referring to the way in which variable turn is used when both threads want to enter their critical sections.

2.1.5 Using the volatile Modifier

As presented above, Peterson’s algorithm may not work in the presence of certain compiler and hardware optimizations that are safe for single-threaded programs but unsafe for multithreaded programs. For example, to optimize speed, the compiler may allow each thread to keep private copies of shared variables intendToEnter0, intendToEnter1, and turn. If this optimization is performed, updates made to these variables by one thread will be made to that thread’s private copies and thus will not be visible to the other thread. This potential inconsistency causes an obvious problem in Peterson’s algorithm (see also Exercise 2.12).

54

THE CRITICAL SECTION PROBLEM

In Java, the solution to this problem is to declare shared variables (that are not also declared as double or long) as volatile:

volatile boolean intendToEnter0 = false; volatile boolean intendToEnter1 = false; volatile int turn;

Declaring the variables as volatile ensures that consistent memory values will be read by the threads. C++ also allows variables to be declared as volatile. (Note that in C++, each element of a volatile array is volatile. In Java, declaring an array object as volatile applies to the array object itself, not its elements.) However, C++ does not make the same strong guarantees that Java does for programs with volatile variables and multiple threads. Thus, even if the variables are volatile, some hardware optimizations may cause Peterson’s algorithm to fail in C++. The general solution to this problem is to add special memory instructions, called memory barriers, that constrain the optimizations. In Section 2.6 we discuss shared memory consistency in greater detail.

Peterson’s algorithm requires volatile variables, but this does not mean that shared variables must always be volatile variables. In later chapters we present high-level synchronization constructs that solve the critical section problem and also deal with optimizations so that volatile variables are not needed.

2.2 TICKET-BASED SOLUTIONS TO THE n-THREAD CRITICAL SECTION PROBLEM

In the n-thread critical section problem, there are n threads instead of just two. Two correct solutions to this problem are given in this section. Both solutions are based on the use of tickets. When a thread wishes to enter a critical section, it requests a ticket. Each ticket has a number on it. Threads are allowed to enter their critical sections in ascending order of their ticket numbers.

The global array number is used to hold the ticket numbers for the threads:

volatile int number[n]; // array of ticket numbers where number[i] is the ticket // number for thread Ti

Initially, all elements of array number have the value 0. If number[i] = 0, then number[i] is the ticket number for thread Ti, 0 ≤ i ≤ n − 1.

2.2.1 Ticket Algorithm

The ticket algorithm uses variables next and permit and a special atomic operation. When a thread requests a ticket, it is given a ticket with a number equal to the value of variable next, and then next is incremented by 1. A thread can enter the critical section when it has a ticket number equal to the value of variable permit. Variable permit is initialized to 1 and is incremented each time a thread enters the critical section.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]