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

modern-multithreading-c-java

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

EXERCISES

175

noFullB.V();

notFullA.V();

// critical section end

Amutex.V();

Bmutex.V();

// now msgB contains A’s message

...;

...;

// and msgA contains B’s message

}

}

}

}

(f)binarySemaphore notFull(1), notEmptyA(0), notEmptyB(0); int shared;

void exchangeWithB() {

void exchangeWithA() {

int msgA;

int msgB, temp;

while (true) {

while (true) {

// create the messages that will be exchanged

msgA = ...;

msgB = ...;

//mutual exclusion for exchange notFull.P();

//A makes copy for B

shared = msgA;

// signal copy made for B

notEmptyA.V(); notEmptyA.P();

//B gets A’s copy temp = shared;

//B makes copy for A shared = msgB;

//signal copy made for A

notEmptyB.P(); notEmptyB.V();

//A gets B’s copy msgA = shared;

//allow next exchange notFull.V();

//now msgB contains A’s message

...;

...;

// and msgA contains B’s message

}

}

}

}

3.15.Alternating threads [Reek 2003]. Consider method alternate(), which two threads call in order to alternate execution with one another. Each time a thread calls alternate(), it signals the other thread and then blocks itself. The signaling thread remains blocked until the other thread calls alternate().

(a)Is the following implementation of method alternate() correct? Explain your answer.

176

SEMAPHORES AND LOCKS

boolean isWaiting = false;

binarySemaphore mutex = new binarySemaphore(1); binarySemaphore block = new binarySemaphore(0); public void alternate() {

mutex.P()

if (isWaiting) {block.V(); block.P();}

else {isWaiting = true; mutex.V(); block.P();} mutex.V();

}

(b)If you believe that this solution is incorrect, describe how to fix it without making any major changes. (For example, do not change the number of semaphores.)

(c)Write another solution to this problem. You may add additional semaphores, but you cannot use operation VP().

3.16.Suppose that reachability testing is applied to program CP. Assume that every execution of CP is checked during reachability testing and that all outputs and all sequences are correct. Can we say then that program CP is correct? Explain.

3.17.The number of sequences exercised during reachability testing for the bounded-buffer program in Listing 3.6 is shown in Table 3.2. There are at least two possible ways to modify the program in Listing 3.6 to allow multiple producers and consumers (see Exercise 3.1). Compare the number of sequences exercised by reachability testing for each program. Which program is the best, considering all factors?

3.18.The countingSemaphore class in Section 3.6.1 may fail if threads blocked on the wait operation in method P() can be interrupted or if spurious wakeups can occur. Describe a sequence of P() and V() operations that leads to a failure if interrupts or spurious wakeups occur. Hint : Assume that semaphore s is initialized to 1 and consider the value that variable permits should have after each P() and V() operation.

3.19.Figures 3.9 and 3.10 shows scenarios in which multiple readers and writers issue requests before any one of them is allowed to read or write. Consider strategy R>W.2 in Section 3.5.4. Where in the code for readers and writers would you consider a “read request” or “write request” event to occur? After identifying the location, determine whether it is possible to have two successive “read request” events or two successive “write request” events without an intervening read or write event. Can the scenarios in Figs. 3.9 and 3.10 occur in the readers and writers solutions presented in Section 3.5.4?

4

MONITORS

Semaphores were defined before the introduction of programming concepts such as data encapsulation and information hiding. In semaphore-based programs, shared variables and the semaphores that protect them are global variables. This causes shared variable and semaphore operations to be distributed throughout the program. Since P and V operations are used for both mutual exclusion and condition synchronization, it is difficult to determine how a semaphore is being used without examining all the code.

Monitors were invented to overcome these problems. The monitor concept was developed by Tony Hoare and Per Brinch Hansen in the early 1970s. This is the same time period in which the concept of information hiding [Parnas 1972] and the class construct [Dahl et al. 1970] originated. Monitors support data encapsulation and information hiding and are adapted easily to an object-oriented environment.

In this chapter we show how to use semaphores to implement monitor classes for C++ and Java. These custom classes support cyclical testing and debugging techniques for monitor-based programs. Even though Java and Pthreads provide built-in support for monitor-like objects, our monitor classes are still useful since it is not easy to test and debug built-in Java or Pthreads monitors. Win32 does not provide monitors, but we can use our custom classes to simulate monitors in C++/Win32 programs.

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.

177

178

MONITORS

4.1 DEFINITION OF MONITORS

A monitor encapsulates shared data, all the operations on the data, and any synchronization required for accessing the data. A monitor has separate constructs for mutual exclusion and condition synchronization. In fact, mutual exclusion is provided automatically by the monitor’s implementation, freeing the programmer from the burden of implementing critical sections. We will use an object-oriented definition of a monitor in which a monitor is a synchronization object that is an instance of a special monitor class. A monitor class defines private variables and a set of public and private access methods. The variables of a monitor represent shared data. Threads communicate by calling monitor methods that access the shared variables. The need to synchronize access to shared variables distinguishes monitor classes from regular classes.

4.1.1 Mutual Exclusion

At most one thread is allowed to execute inside a monitor at any time. However, it is not the programmer’s responsibility to provide mutual exclusion for the methods in a monitor. Mutual exclusion is provided by the monitor’s implementation, using one of the techniques discussed in previous chapters. If a thread calls a monitor method but another thread is already executing inside the monitor, the calling thread must wait outside the monitor. A monitor has an entry queue to hold the calling threads that are waiting to enter the monitor (see Fig. 4.2).

4.1.2 Condition Variables and SC Signaling

Condition synchronization is achieved using condition variables and operations wait() and signal(). A condition variable denotes a queue of threads that are waiting for a specific condition to become true. (The condition is not explicitly specified as part of the condition variable.) A condition variable cv is declared as

condition Variable cv;

Operation cv.wait() is analogous to a P operation in that it is used to block a thread. Operation cv.signal() unblocks a thread and is analogous to a V operation. But as we will see below, operations wait() and signal() do more than just block and unblock threads.

A monitor has one entry queue plus one queue associated with each condition variable. For example, Listing 4.1 shows the structure of monitor class boundedBuffer. Class boundedBuffer inherits from class monitor. It has five data members, condition variables named notFull and notEmpty, and monitor methods deposit() and withdraw(). Figure 4.2 is a graphical view of class boundedBuffer, which shows its entry queue and the queues associated with condition variables notFull and notEmpty.

DEFINITION OF MONITORS

179

class boundedBuffer extends monitor { public void deposit(...) { ... } public int withdraw (...) { ... } public boundedBuffer( ) { ... }

private int fullSlots = 0;

// # of full slots in the buffer

private int capacity = 0;

// capacity of the buffer

private int [] buffer = null;

// circular buffer of ints

//in is index for next deposit, out is index for next withdrawal private int in = 0, out = 0;

//producer waits on notFull when the buffer is full

private conditionVariable notFull;

// consumer waits on notEmpty when the buffer is empty private conditionVariable notEmpty;

}

Listing 4.1 Monitor class boundedBuffer .

deposit() {..}

notFull

entry queue

withdraw() {..}

 

notEmpty

Figure 4.2 Graphical view of monitor class boundedBuffer.

A thread that is executing inside a monitor method blocks itself on condition variable cv by executing

cv.wait();

Executing a wait() operation releases mutual exclusion (to allow another thread to enter the monitor) and blocks the thread on the rear of the queue for cv. The threads blocked on a condition variable are considered to be outside the monitor. If a thread that is blocked on a condition variable is never awakened by another thread, a deadlock occurs.

A thread blocked on condition variable cv is awakened by the execution of

cv.signal();

If there are no threads blocked on cv, the signal() operation has no effect; otherwise, the signal() operation awakens the thread at the front of the queue for cv. What happens next depends on exactly how the signal() operation is defined. There are several different types of signaling disciplines. For now, we assume that the signal-and-continue (SC) discipline is used. This is the discipline used

180 MONITORS

by Java’s built-in monitor construct. Other types of signals are described in Section 4.4.

After a thread executes an SC signal to awaken a waiting thread, the signaling thread continues executing in the monitor and the awakened thread is moved to the entry queue. That is, the awakened thread does not reenter the monitor immediately; rather, it joins the entry queue and waits for its turn to enter. When SC signals are used, signaled threads have the same priority as threads trying to enter the monitor via public method calls.

Let A denote the set of threads that have been awakened by signal() operations and are waiting to reenter the monitor, S denote the set of signaling threads, and C denote the set of threads that have called a monitor method but have not yet entered the monitor. (The threads in sets A and C wait in the entry queue.) Then in an SC monitor, the relative priority associated with these three sets of threads is S > C = A.

Operation cv.signalAll() wakes up all the threads that are blocked on condition variable cv. Operations empty() and length() return information about the queue associated with a condition variable. The execution of

cv.empty()

returns true if the queue for cv is empty, and false otherwise. Executing

cv.length()

returns the current length of the queue for cv.

Listing 4.3 shows a complete boundedBuffer monitor. This solution uses condition variables notEmpty and notFull. Operations deposit() and withdraw() check the state of the buffer, perform their buffer operations, and signal each other at the end. As shown, the signal() statements at the ends of the methods are always executed. Alternatively, since signal() operations may involve context switches and thus may be relatively expensive, they can be guarded with if- statements so that they are only executed when it is possible that another thread is waiting.

Assume that the buffer is empty and that the thread at the front of the entry queue is Consumer1 (C1). The queues for condition variables notFull and notEmpty are also assumed to be empty (Fig. 4.4a). When Consumer1 enters method withdraw(), it executes the statement

while (fullSlots == 0) notEmpty.wait();

Since the buffer is empty, Consumer1 blocks itself by executing a wait() operation on condition variable notEmpty (Fig. 4.4b).

Producer1 (P1) then enters the monitor. Since the buffer is not full, Producer1 deposits an item and executes notEmpty.signal(). This signal operation awakens Consumer1 and moves Consumer1 to the rear of the entry queue behind

DEFINITION OF MONITORS

181

class boundedBuffer extends monitor {

private int fullSlots = 0;

// number of full slots in the buffer

private int capacity = 0;

// capacity of the buffer

private int[] buffer = null;

// circular buffer of ints

private int in = 0, out = 0;

private conditionVariable notFull = new conditionVariable(); private conditionVariable notEmpty = new conditionVariable();

public boundedBuffer(int bufferCapacity ) {

capacity = bufferCapacity;buffer = new int[bufferCapacity];} public void deposit(int value) {

while (fullSlots == capacity) notFull.wait();

buffer[in] = value;

in = (in + 1) % capacity; ++fullSlots;

notEmpty.signal(); //alternatively:if (fullSlots == 1) notEmpty.signal();

}

public int withdraw() { int value;

while (fullSlots == 0) notEmpty.wait();

value = buffer[out];

out = (out + 1) % capacity; --fullSlots;

notFull.signal(); //alternatively:if (fullSlots == capacity–1) notFull.signal(); return value;

}

 

}

 

...

 

boundedBuffer bb;

 

bb.deposit(...);

// executed by Producers

bb.withdraw(...);

// executed by Consumers

 

Listing 4.3 Monitor class boundedBuffer .

Consumer2 (C2) (Fig. 4.4c). After its signal() operation, Producer1 can continue executing in the monitor, but since there are no more statements to execute, Producer1 exits the monitor. Consumer2 now barges ahead of Consumer1 and consumes an item. Consumer2 executes notFull.signal(), but there are no Producers waiting, so the signal has no effect. When Consumer2 exits the monitor, Consumer1 is allowed to reenter, but the loop condition (fullSlots == 0 ) is true again:

while (fullSlots == 0) notEmpty.wait();

182

 

 

 

 

 

 

 

 

MONITORS

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

deposit() {..}

 

 

 

 

 

deposit() {..}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

C2 P1 C1

wait

C2 P1

notFull

 

 

 

withdraw() {..}

 

 

 

 

 

withdraw() {..}

 

 

 

 

 

entry queue

 

 

 

 

 

entry queue

 

 

 

 

 

 

 

 

 

 

C1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

notEmpty

 

 

notEmpty

 

 

 

 

 

 

(a)

 

(b)

 

 

 

 

 

 

 

 

 

 

deposit() {P1}

 

 

 

 

 

deposit() {..}

 

 

 

 

 

 

 

 

 

 

 

 

C1 C2

notFull

 

notFull

 

 

 

 

entry queue

withdraw() {..}

 

 

 

 

entry queue

withdraw() {..}

 

 

 

 

 

 

C1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

notEmpty

 

 

notEmpty

 

 

 

 

 

 

(c)

 

(d)

 

Figure 4.4 Bounded buffer monitor.

Thus, Consumer1 is blocked once

more on condition variable notEmpty

(Fig. 4.4d ). Even though Consumer1 entered the monitor first, it is Consumer2 that consumes the first item.

This example illustrates why the wait() operations in an SC monitor are usually found inside while-loops: A thread waiting on a condition variable cannot assume that the condition it is waiting for will be true when it reenters the monitor.

4.2 MONITOR-BASED SOLUTIONS TO CONCURRENT PROGRAMMING PROBLEMS

We now show several monitor-based solutions to classical concurrent programming problems. These solutions assume that condition variable queues are first- come-first-serve (FCFS).

4.2.1 Simulating Counting Semaphores

Solution 1 Listing 4.5 shows an SC monitor with methods P() and V() that simulates a counting semaphore. In this implementation, a waiting thread may get stuck forever in the while-loop in method P(). To see this, assume that the value of permits is 0 when thread T1 calls P(). Since the loop condition (permits == 0) is true, T1 will block itself by executing a wait operation. Now assume that some other thread executes V() and signals T1. Thread T1 will join the entry queue behind any threads that have called P() and are waiting to enter the monitor for the first time. These other threads can enter the monitor and decrement permits before T1 has a chance to reenter the monitor and examine its loop condition. If the value of permits is 0 when T1 eventually evaluates its loop condition, T1 will block itself again by issuing another wait operation.

MONITOR-BASED SOLUTIONS TO CONCURRENT PROGRAMMING PROBLEMS

183

class countingSemaphore1 extends monitor {

private int permits; // The value of permits is never negative.

private conditionVariable permitAvailable = new conditionVariable(); public countingSemaphore1(int initialPermits) { permits = initialPermits;} public void P() {

while (permits == 0) permitAvailable.wait();

--permits;

}

public void V() { ++permits; permitAvailable.signal();

}

}

// Threads call methods P() and V(): countingSemaphore1 s;

s.P();

s.V();

Listing 4.5 Class countingSemaphore1 .

Solution 2 The SC monitor in Listing 4.6 is based on Implementation 2 in Listing 3.3. Unlike solution 1, this solution does not suffer from a starvation problem. Threads that call P() cannot barge ahead of signaled threads and “steal” their permits. Consider the scenario that we described in Solution 1. If thread T1 calls P() when the value of permits is 0, T1 will decrement permits to −1 and block itself by executing a wait operation. When some other thread executes V(), it will increment permits to 0 and signal T1. Threads ahead of T1 in the entry queue can enter the monitor and decrement permits before T1 is allowed to reenter the monitor. However, these threads will block themselves on the wait operation in P(), since permits will have a negative value. Thread T1 will eventually be allowed to reenter the monitor, and since there are no statements to execute after the wait operation, T1 will complete its P() operation. Thus, in this solution, a waiting thread that is signaled is guaranteed to get a permit.

4.2.2 Simulating Binary Semaphores

The SC monitor in Listing 4.7 is based on Implementation 3 in Listing 3.4. Threads in P() wait on condition variable allowP, while threads in V() wait on condition variable allowV. Waiting threads may get stuck forever in the while- loops in methods P() and V().

4.2.3 Dining Philosophers

Solution 1 The SC monitor in Listing 4.8 is similar to Solution 4 (using semaphores) in Section 3.5.3. A philosopher picks up two chopsticks only if

184

MONITORS

class countingSemaphore2 extends monitor {

private int permits; // The value of permits may be negative.

private conditionVariable permitAvailable = new conditionVariable(); public countingSemaphore2(int initialPermits) { permits = initialPermits;} public void P() {

--permits;

if (permits < 0) permitAvailable.wait();

}

public void V() { ++permits; permitAvailable.signal();

}

}

Listing 4.6 Class countingSemaphore2 .

class binarySemaphore extends monitor { private int permits;

private conditionVariable allowP = new conditionVariable(); private conditionVariable allowV = new conditionVariable();

public binarySemaphore(int initialPermits) { permits = initialPermits;} public void P() {

while (permits == 0) allowP.wait();

permits = 0; allowV.signal();

}

public void V() {

while (permits == 1) allowV.wait();

permits = 1; allowP.signal();

}

}

Listing 4.7 Class binarySemaphore.

both of them are available. Each philosopher has three possible states: thinking, hungry, and eating. A hungry philosopher can eat if her two neighbors are not eating. A philosopher blocks herself on a condition variable if she is hungry but unable to eat. After eating, a philosopher will unblock a hungry neighbor who is able to eat. This solution is deadlock-free but not starvation-free, since a philosopher can starve if one of its neighbors is always eating (Exercise 4.25). However, the chance of a philosopher starving may be so highly unlikely that perhaps it can be safely ignored [Turski 1991].

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