modern-multithreading-c-java
.pdfMESSAGE-BASED SOLUTIONS TO CONCURRENT PROGRAMMING PROBLEMS |
275 |
žIf several accept() alternatives are open and have waiting entry calls, the one whose entry call arrived first is selected.
žif one or more accept() alternatives are open but none have a waiting entry call, choose() blocks until an entry call arrives for one of the open accept() alternatives.
When the selectiveWait has an else or delay alternative:
žAn else alternative is executed if all the accept() alternatives are closed or all the open accept() alternatives have no waiting entry calls.
žAn open delay alternative is selected when its expiration time is reached if no open accept() alternatives can be selected prior to the expiration time.
When all of the guards of the accept alternatives are false and there is no delay alternative with a true guard and no else alternative, method choose() throws a SelectException indicating that a deadlock has been detected.
Listing 5.10 shows a boundedBuffer server class that uses a selective wait. The delayAlternative simply displays a message when it is accepted. Note that the guards of all the alternatives are evaluated each iteration of the while-loop. Changes made to variables fullSlots and emptySlots in the alternatives of the switch statement may change the values of the guards for entries deposit and withdraw. This requires each guard to be reevaluated before the next selection occurs.
If a boundedBuffer thread is marked as a Java daemon thread before it is started, it will stop automatically when its clients have stopped. A C++ version of class boundedBuffer would look very similar to the Java version, without the daemon threads. Instead, we can use a delayAlternative to detect when the clients have stopped.
case 3: delayA.accept(); // terminate the boundedBuffer thread when it // becomes inactive for some period
return;
The delayAternative will stop the boundedBuffer thread after a period of inactivity.
5.4 MESSAGE-BASED SOLUTIONS TO CONCURRENT PROGRAMMING PROBLEMS
Below we show solutions to three classical synchronization problems. All three solutions use message passing with selective waits.
5.4.1 Readers and Writers
Listing 5.11 shows a solution to the readers and writers problem for strategy R>W.1 (many readers or one writer, with readers having a higher priority). Class
276 |
MESSAGE PASSING |
final class boundedBuffer extends Thread { private selectableEntry deposit, withdraw; private int fullSlots=0; private int capacity = 0;
private Object[] buffer = null; private int in = 0, out = 0;
public boundedBuffer(selectableEntry deposit, selectableEntry withdraw, int capacity) {
this.deposit = deposit; this.withdraw = withdraw; this.capacity = capacity; buffer = new Object[capacity];
}
public void run() { try {
selectiveWait select = new selectiveWait(); selectiveWait.delayAlternative delayA =
select.new delayAlternative(500);
select.add(deposit); |
|
// alternative 1 |
select.add(withdraw); |
// alternative 2 |
|
select.add(delayA); |
|
// alternative 3 |
while(true) { |
|
|
withdraw.guard(fullSlots>0); |
||
deposit.guard (fullSlots<capacity); |
||
delayA.guard(true); |
|
|
switch (select.choose()) { |
|
|
case 1: |
Object o = deposit.acceptAndReply(); |
|
|
buffer[in] = o; |
|
|
in = (in + 1) % capacity; ++fullSlots; |
|
|
break; |
|
case 2: |
withdraw.accept(); |
|
|
Object value = buffer[out]; |
|
|
withdraw.reply(value); |
|
|
out = (out + 1) % capacity; --fullSlots; |
|
|
break; |
|
case 3: |
delayA.accept(); |
|
|
System.out.println("delay selected"); |
|
|
break; |
|
} |
|
|
} |
|
|
}catch (InterruptedException e) {}
catch (SelectException e) {System.out.println("deadlock detected"); System.exit(1); }
}
}
Listing 5.10 Java bounded buffer using a selectiveWait .
MESSAGE-BASED SOLUTIONS TO CONCURRENT PROGRAMMING PROBLEMS |
277 |
final class Controller extends Thread {
//Strategy R>W.1 : Many readers or one writer; readers have a higher priority private selectablePort startRead = new selectablePort ();
private selectablePort endRead = new selectablePort (); private selectablePort startWrite = new selectablePort (); private selectablePort endWrite = new selectablePort (); private boolean writerPresent = false;
private int readerCount = 0; private int sharedValue = 0;
public int read() {
try {startRead.send();} catch(Exception e) {} int value = sharedValue;
try {endRead.send();} catch(Exception e) {} return value;
}
public void write(int value) {
try {startWrite.send();} catch(Exception e) {} sharedValue = value;
try {endWrite.send();} catch(Exception e) {}
}
public void run() { try {
selectiveWait select = new selectiveWait();
select.add(startRead); |
// alternative 1 |
|
select.add(endRead); |
// alternative 2 |
|
select.add(startWrite); |
// alternative 3 |
|
select.add(endWrite); |
// alternative 4 |
|
while(true) { |
|
|
startRead.guard(!writerPresent); |
|
|
endRead.guard(true); |
|
|
startWrite.guard(!writerPresent && readerCount == 0 && |
||
startRead.count() == 0); |
|
|
endWrite.guard(true); |
|
|
switch (select.choose()) { |
|
|
case 1: |
startRead.receive(); |
|
|
++readerCount; |
|
|
break; |
|
case 2: |
endRead.receive(); |
|
|
--readerCount; |
|
|
break; |
|
Listing 5.11 Readers and writers using a selectiveWait .
278 |
MESSAGE PASSING |
case 3: startWrite.receive(); writerPresent = true; break;
case 4: endWrite.receive(); writerPresent = false; break;
}
}
} catch (InterruptedException e) {}
}
}
Listing 5.11 (continued )
Controller uses a selectiveWait with selectablePort objects startRead, endRead, startWrite, and endWrite. A selectablePort object is a synchronous port that can be used in selective waits. The guard for startRead is (!writerPresent ), which ensures that no writers are writing when a reader is allowed to start reading. The guard for startWrite is (!writerPresent && readerCount == 0 && startRead. count() == 0). This allows a writer to start writing only if no other writer is writing, no readers are reading, and no reader is waiting for its call to startRead to be accepted. The call to startRead.count() returns the number of startRead.send() operations that are waiting to be received.
Notice that the selectablePorts are private members of Controller and thus cannot be accessed directly by reader and writer threads. Instead, readers and writers call public methods read() and write(), respectively. These public methods just pass their calls on to the private entries, ensuring that the entries are called in the correct order (i.e., startRead is called before endRead, and startWrite is called before endWrite).
5.4.2 Resource Allocation
Listing 5.12 shows a solution to the resource allocation problem. A resourceServer manages three resources. A client calls entry acquire to get a resource and entry release to return the resource when it is finished. Vector resources contains the IDs of the available resources. The integer available is used to count the number of resources available. A resource can be acquired if the guard (available > 0) is true. A resource’s ID is given to the client that acquires the resource. The client gives the resource ID back when it releases the resource. Notice that both case alternatives contain statements that occur after the server replies to the client. Replying early completes the rendezvous with the client and allows the client to proceed as soon as possible.
Listing 5.13 shows an SU monitor solution for this same resource allocation problem. Monitor resourceMonitor and thread resourceServer demonstrate a mapping between monitors and server threads. Thread resourceServer is an
MESSAGE-BASED SOLUTIONS TO CONCURRENT PROGRAMMING PROBLEMS |
279 |
final class resourceServer extends Thread { private selectableEntry acquire; private selectableEntry release;
private final int numResources = 3; private int available = numResources;
Vector resources = new Vector(numResources);
public resourceServer(selectableEntry acquire, selectableEntry release) { this.acquire = acquire;
this.release = release; resources.addElement(new Integer(1)); resources.addElement(new Integer(2)); resources.addElement(new Integer(3));
}
public void run() { int unitID;
try {
selectiveWait select = new selectiveWait();
select.add(acquire); |
// alternative 1 |
select.add(release); |
// alternative 2 |
while(true) { acquire.guard(available > 0); release.guard(true);
switch (select.choose()) { case 1: acquire.accept();
unitID = ((Integer) resources.firstElement()).intValue(); acquire.reply(new Integer(unitID)); --available; resources.removeElementAt(0); break;
case 2: unitID = ((Integer) release.acceptAndReply()).intValue(); ++available;
resources.addElement(new Integer(unitID)); break;
}
}
} catch (InterruptedException e) {}
}
}
Listing 5.12 Resource allocation using a selectiveWait .
280 |
MESSAGE PASSING |
final class resourceMonitor extends monitorSU {
private conditionVariable freeResource = new conditionVariable(); private int available = 3;
Vector resources = new Vector(3); public resourceMonitor() {
resources.addElement(new Integer(1)); resources.addElement(new Integer(2)); resources.addElement(new Integer(3));
}
public int acquire() { int unitID; enterMonitor();
if (available == 0) freeResource.waitC();
else
--available;
unitID = ((Integer)resources.firstElement()).intValue(); resources.removeElementAt(0);
exitMonitor(); return unitID;
}
public void release(int unitID) { enterMonitor();
resources.addElement(new Integer(unitID)); if (freeResource.empty()) {
++available;
exitMonitor();
}
else freeResource.signalC_and_exitMonitor();
}
}
Listing 5.13 Resource allocation using a monitor.
active object that executes concurrently with the threads that call it. The resourceMonitor is a passive object, not a thread, which does not execute until it is called. A monitor cannot prevent a thread from entering one of its methods (although the monitor can force threads to enter one at a time). However, once a thread enters the monitor, the thread may be forced to wait on a condition variable until a condition becomes true. Condition synchronization in a server thread works in the opposite way. A server thread will prevent an entry call from being accepted until the condition for accepting the call becomes true.
The conversion between a monitor and a server thread is usually simple (except for cases where the server contains a timeout alternative). It has been shown
TRACING, TESTING, AND REPLAY FOR MESSAGE-PASSING PROGRAMS |
281 |
final class countingSemaphore extends Thread { private selectablePort V, P;
private int permits;
public countingSemaphore(int initialPermits) {permits = initialPermits;} public void P() { P.send();}
public void V() {V.send();} public void run() {
try {
selectiveWait select = new selectiveWait();
select.add(P); |
// alternative1 |
select.add(V); |
// alternative 2 |
while(true) { |
|
P.guard(permits>0); |
|
V.guard(true); |
|
switch (select.choose()) { |
|
case 1: P.receive(); |
|
--permits; |
|
break; |
|
case 2: V.receive(); |
|
++permits; |
|
break; |
|
} |
|
} |
|
} catch (InterruptedException e) {} |
|
}
}
Listing 5.14 Using a selectiveWait to simulate a counting semaphore.
elsewhere that communication using shared variables and communication using message passing are equivalent (i.e., they have the same expressive power). Thus, a program that uses shared variables with semaphores or monitors can be transformed into an equivalent program that uses message passing, and vice versa.
5.4.3 Simulating Counting Semaphores
Listing 5.14 shows an implementation of a countingSemaphore that uses a selectiveWait with selectablePorts named P and V . Clients call public methods P() and V(), which pass the calls on to the (private) ports. A P() operation can be performed only if the guard (permits > 0) is true.
5.5 TRACING, TESTING, AND REPLAY FOR MESSAGE-PASSING PROGRAMS
In this section we define SYN-sequences for channel-based programs and present solutions to the replay and feasibility problems. We show how to modify the
282 |
MESSAGE PASSING |
channel classes so that they can be used to trace, replay, and check the feasibility of SYN-sequences. We will also describe how to perform reachability testing for programs that use the channel classes.
5.5.1 SR-Sequences
We begin by defining an object-based SYN-sequence of a message-passing program, which means that there is one SYN-sequence for each synchronization object in the program. A thread-based SYN-sequence, which has one SYNsequence for each thread, is defined after that.
Object-Based SR-Sequences Let CP be a concurrent program that uses channels. Assume for now that CP has no selective wait statements. The synchronization objects in CP are its channels, and the synchronization events in CP are executions of send() and receive() operations on these channels. A SYNsequence for a channel is a sequence of send and receive events (or SR-events) of the following types:
žSRsynchronization: a synchronization between send and receive operations on a synchronous channel
žasynchronousSendArrival: an arrival of an asynchronous send operation whose message is eventually received
žasynchronousReceive: a receive operation that eventually receives a message on an asynchronous channel
žunacceptedSend: a synchronous send operation whose message is never received
žunacceptedAsynchSend: an asynchronous send operation whose message is never received
žunacceptedReceive: a receive on an asynchronous or synchronous channel that never receives a message
žsendException: a send operation on an asynchronous or synchronous channel that causes an exception to be thrown
žreceiveException: a receive operation on an asynchronous or synchronous channel that causes an exception to be thrown
žstartEntry: the start of a rendezvous on an entry
žendEntry: the end of a rendezvous on an entry
Notice that for asynchronous messages, there are arrival events but no send events. This is because it is the order in which messages arrive that determines the result of an execution, not the order in which messages are sent. As we will see later, send events are needed for reachability testing, where they are used to identify the race conditions that occur during an execution.
An object-based SR-event for channel C is denoted by
(S, R, NS, NR, eventType)
TRACING, TESTING, AND REPLAY FOR MESSAGE-PASSING PROGRAMS |
283 |
where
žS is the sending thread of a send operation or the calling thread of an entry call.
žR is the receiving thread of a receive operation or the accepting thread of an entry call.
žNS is the sender’s order number, which gives the relative order of this event among all of the sending/calling thread’s events.
žNR is the receiver’s order number, which gives the relative order of this event among all of the receiving/accepting thread’s events.
ževentType is one of the send–receive event types, which are listed above.
The order number of an event executed by thread T gives the relative order of this event among all the events exercised by T . For example, an event T with order number 2 is the second event exercised by T . For asynchronousSendArrival, unacceptedSend , and sendException events, R and NR are not applicable (NA) since no receiving thread is involved with these events. Similarly, for unacceptedReceive and receiveException events, S and NS are not applicable since there is no sending thread. The order number of an asynchronousSendArrival event is for the send operation of the thread that generated it.
The program in Listing 5.15 shows why order numbers are needed in SRevents. Assume that order numbers are not specified. Then the only feasible SR-sequence for channel C1 (without order numbers) contains the single event
(Thread1, Thread2, SRsynchronization).
The only feasible SR-sequence for C2 also contains a single event:
(Thread1, Thread3, SRsynchronization).
Suppose now that we create a new program by reversing the order of the send operations in Thread1 :
Thread1 Thread2 Thread3
C2.send(); C1.receive(); C2.receive();
C1.send();
mailbox C1, C2; // synchronous mailboxes
Thread1 |
Thread2 |
Thread3 |
C1.send(); |
C1.receive(); |
C2.receive(); |
C2.send(); |
|
|
Listing 5.15 Demonstrating the need for order numbers.
284 |
MESSAGE PASSING |
Then the feasible SR-sequences (without order numbers) for channels C1 and C2 in the new program are the same as in the original program, even though the two programs are different in an obvious and important way.
Adding order numbers to the SR-events distinguishes between these two different programs. The SR-sequence for C1 in the first program becomes
(Thread1, Thread2, 1, 1, SRsynchronization). |
// first event of Thread1 and |
|
// first event of Thread2 |
and the SR-sequence for C1 in the second program becomes |
|
(Thread1, Thread2, 2, 1, SRsynchronization). |
// second event of Thread1 and |
|
// first event of Thread2 |
When order numbers are specified, an SR-sequence of C1 that is feasible for the first program will be infeasible for the second, and vice versa.
Given the format above for an object-based SR-event, an SR-sequence of channel C is denoted as
C: ((S1, R1, NS1, NR1, eventType1), (S2, R2, NS2, NR2, eventType2), ...)
where (Si, Ri, NSi, NRi, eventTypei) denotes the ith, i > 0, SR-event in the SRsequence of C. An object-based SR-sequence of a program contains an SR-
sequence for each channel in the program.
Thread-Based SR-Sequences We now define two formats for thread-based SRsequences. Each thread in the program has its own SR-sequence. There are two possible formats for the SR-events in a thread-based SR-sequence.
Format 1 A thread-based SR-event for thread T is denoted by
(C, NC, eventType)
where
žC is the channel name.
žNC is the channel order number.
ževentType is the type of the event.
The channel order number NC gives the relative order of this event among all the events involving channel C. The list of event types is the same as that above for object-based events, except that we remove type SRsynchronization and add separate event types for synchronous send and receive operations:
žSend: a synchronous send operation executed by thread T where the message is eventually accepted
