modern-multithreading-c-java
.pdfEXERCISES |
255 |
(b)Write an SC monitor implementation of method waitB(). (Be careful to make sure that new threads will be delayed if there are old threads that have been signaled but have not yet had a chance to leave the barrier.)
4.21.In Listing 4.11, assume that there are many threads blocked in methods P() and V(). Suppose that a thread executes the notifyAll operation in method V(). One notified thread that was waiting in P() may find that the condition it is waiting for is true. Are all the other notified threads (that were waiting in P() or V()) guaranteed to find that the condition they are waiting for is false?
4.22.Suppose that monitor alternateThreads below uses either an SC toolbox (Listing 4.23) or a USC toolbox (see Exercise 4.15) that is implemented without VP() operations. (That is, toolboxes monitorSCNoVP and monitorUSCNoVP use separate V() and P() operations in place of a VP() operation.) Method alternate() forces two threads to alternate execution with one another. Each time a thread calls alternate() it signals condition other (unblocking the other thread) and then blocks itself. The thread remains blocked until the other thread calls alternate(). Suppose that these toolboxes use strong FCFS semaphores (and no VP() operations, as mentioned above). Is monitor alternateThreads correct? If not, show a scenario that illustrates the problem.
final class alternateThreads extends monitorSCNoVP /*or extends monitorUSCNoVP*/ {
conditionVariable other = new conditionVariable(); public final void alternate() {
other.signalC();
other.waitC();
}
}
4.23.The SC toolbox in Listing 4.23 uses operation VP(). Show how to achieve the same effect without using VP(). That is, without using VP(), make sure that threads are signaled in the same order that they execute waitC(). To help you design a solution, you may want to examine the FCFS implementations of classes countingSemaphore and binarySemaphore.
4.24.Below is an SC monitor implementation of strategy R>W.1, which allows concurrent reading and gives readers a higher priority than writers. Variable stopBarging is used to prevent barging writers from writing when the signaled reader is waiting in the entry queue and no more readers are waiting in the readerQ Is this solution correct? If two readers are waiting when a writer executes endWrite(), are both readers guaranteed to read before another writer can write?
256 |
MONITORS |
// number of active readers // true if a writer is writing
// set to true to stop writers from barging // ahead of readers
conditionVariable readerQ = new conditionVariable(); conditionVariable writerQ = new conditionVariable(); public void startRead() {
if (writing) readerQ.wait();
++readerCount; stopBarging = false;
readerQ.signal();
}
public void endRead() { --readerCount;
if (readerCount == 0)
writerQ.signal();
}
public void startWrite() {
//writers wait if a writer is writing, or a reader is reading or waiting,
//or the writer is barging
while (readerCount > 0 || writing || !readerQ.empty() || stopBarging) writerQ.wait();
writing = true;
}
public void endWrite() { writing = false;
if (!readerQ.empty()) { // priority is given to waiting readers readerQ.signal();
stopBarging = true; // if writers barge ahead of the
// signaled reader, they will be delayed
}
else
writerQ.signal();
}
}
4.25.For the solutions to the dining philosophers problem in Section 4.2.3:
(a)Give a scenario for Solution 1 that illustrates how a philosopher can starve.
(b)Show how this scenario is avoided in Solution 2.
EXERCISES |
257 |
4.26.In Exercise 3.18 you were asked to give a scenario that demonstrates how class countingSemaphore 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 how this scenario is prevented by the countingSemaphore class in Listing 4.11.
4.27.Solve the problem in Exercise 4.12 using either Java or Pthreads condition variables.
4.28.Implement the SC monitor toolbox for Win32 from Section 4.8.1 using Win32 Semaphores and Events. Use Semaphores for mutual exclusion. Threads that block in waitC() should block on a Win32 Event. You can use operation SignalObjectAndWait() (see Section 3.7.5) in place of VP().
5
MESSAGE PASSING
In Chapters 3 and 4, threads used shared variables to communicate and they used semaphores, locks, and monitors to synchronize. Threads can also communicate and synchronize by sending and receiving messages across channels. A channel is an abstraction of a communication path between threads. If shared memory is available, channels can be implemented as objects that are shared by threads. Without shared memory, channels can be implemented using kernel routines that transport messages across a communication network.
Chapter 6 deals with message passing for distributed programs, in which messages are passed between processes that run on separate nodes in a network. The focus of this chapter is message passing between threads that run in the same process. Programming problems will be solved by sending and receiving messages across channels instead of reading and writing shared variables protected by semaphores and monitors. This offers us a new style for solving familiar problems. First, we describe basic message passing using send and receive commands. Then we will see how to use a higher-level mechanism called rendezvous. Finally, we show how to test and debug message-passing programs.
5.1 CHANNEL OBJECTS
Threads running in the same program (or process) can access channel objects in shared memory. If the programming language does not provide built-in channel objects, a channel class can be defined at the user level. In the program
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.
258
CHANNEL OBJECTS |
259 |
below, Thread1 and Thread2 use methods send() and receive() to send and receive messages through two channel objects. Thread1 sends requests through the requestChannel, and Thread2 responds through the replyChannel :
channel requestChannel = new channel(); channel replyChannel = new channel();
Thread1 |
Thread2 |
requestChannel.send(request); request = requestChannel.receive(); reply = replyChannel.receive(); replyChannel.send(reply);
A thread that calls send() or receive() may be blocked. Thus, send and receive operations are used both for communication and synchronization. Several types of send and receive operations can be defined:
žBlocking send: The sender is blocked until the message is received (by a receive() operation).
žBuffer-blocking send: Messages are queued in a bounded message buffer, and the sender is blocked only if the buffer is full.
žNonblocking send: Messages are queued in an unbounded message buffer, and the sender is never blocked.
žBlocking receive: The receiver is blocked until a message is available.
žNonblocking receive: The receiver is never blocked. A receive command returns an indication of whether or not a message was received.
Asynchronous message passing occurs when blocking receive operations are used with either nonblocking or buffer-blocking send operations. Synchronous message passing is the term used when the send and receive operations are both blocking. When synchronous message passing is used, either the sender or the receiver thread will be blocked, whichever one executes its operation first. Even if the receiver is waiting for a message to arrive, the sender may be blocked since the sender has to wait for an acknowledgment that the message was received. Thus, there are more delays associated with synchronous message passing.
Synchronous message passing can be simulated using asynchronous message passing. For instance, if a blocking send command is not available, the sender can issue a buffer-blocking send followed immediately by a blocking receive. This also works in cases where the message sent to a receiver represents a request for the receiver to perform some service. The receiver can examine the request, perform the service at the appropriate time, and then send a reply to the waiting sender.
5.1.1 Channel Objects in Java
Threads in the same Java Virtual Machine (JVM) can communicate and synchronize by passing messages through user-defined channels that are implemented
260 |
MESSAGE PASSING |
as shared objects. We have written Java classes that implement several different types of channels. Interface channel specifies the form of send() and receive() operations supported by channel objects.
public abstract class channel { |
|
public abstract void send(Object m); |
// send a message object |
public abstract void send(); |
// send with no message object |
|
// acts as a signal to the receiver |
public abstract Object receive(); |
// receive an object |
} |
|
There are three types of channels, which differ in the number of sender and receiver threads that are allowed to access a channel object:
žMailbox: Many senders and many receivers may access a mailbox object.
žPort: Many senders but only one receiver may access a port object.
žLink: Only one sender and one receiver may access a link object.
Each of the three types of channels has a synchronous version and an asynchronous version. A synchronous mailbox class is shown in Listing 5.1. Operations send() and receive() are implemented using a member variable named message and two binary semaphores. A sending thread copies its message into the channel’s message object and issues sent.V() to signal that the message is available. The sending thread then executes received.P() to wait until the message is received. The receiving thread executes sent.P() to wait for a message from the sender. When the sender signals that a message is available, the receiver makes a copy of the message object and executes received.V() to signal the sender that the message has been received.
For convenience, mailbox provides a send() operation with no message parameter, which sends a null message. A send() operation with no message acts as a signal to the receiver that some event has occurred. (In other words, the signal is the message.) Such a send is analogous to a V () operation on a semaphore, which allows one thread to signal another but does not exchange any data between the threads.
The send() methods for classes port and mailbox are the same. The receive() methods for classes port and link are also the same. Only one thread can ever execute a receive operation on a port or link object. In Listing 5.2 we have modified the receive method of Listing 5.1 to check for multiple receivers. An exception is thrown if multiple receivers are detected. Since a link can have only one sender, a similar check is performed in the send() method of class link.
An asynchronous mailbox class is shown in Listing 5.3. The implementation of the buffer-blocking send() operation is based on the bounded-buffer solution in Section 3.5.2. The send() will block if the message buffer is full. The messages that a thread sends to a particular mailbox are guaranteed to be received in the order they are sent. If Thread1 executes a send() operation on a particular mailbox
CHANNEL OBJECTS |
261 |
public class mailbox extends channel { private Object message = null;
private final Object sending = new Object(); private final Object receiving = new Object();
private final binarySemaphore sent = new binarySemaphore(0); private final binarySemaphore received = new binarySemaphore(0); public final void send(Object sentMsg) {
if (sentMsg == null) {throw new NullPointerException("Null message passed to send()");
} |
|
synchronized (sending) { |
|
message = sentMsg; |
|
sent.V(); |
// signal that the message is available |
received.P(); |
// wait until the message is received |
} |
|
} |
|
public final void send() { |
|
synchronized (sending) { |
|
message = new Object(); |
// send a null message |
sent.V(); |
// signal that message is available |
received.P(); |
// wait until the message is received |
} |
|
} |
|
public final Object receive() { |
|
Object receivedMessage = null; |
|
synchronized (receiving) { |
|
sent.P(); |
// wait for message to be sent |
receivedMessage = message; |
|
received.V(); |
// signal the sender that the message has |
} |
// been received |
return receivedMessage; |
|
} |
|
} |
|
Listing 5.1 Synchronous mailbox class.
before Thread2 executes a send() operation on the same mailbox, Thread1’s message will be received from that mailbox before Thread2’s message.
The asynchronous link and port classes are very similar to the mailbox class. In the asynchronous link class, methods send() and receive() must check for multiple senders and receivers. In the asynchronous port class, method receive must check for multiple receivers. Since links have a single sender, there is no need for semaphore senderMutex in method send. Similarly, there is no need for semaphore receiverMutex in the receive() methods of classes link and port.
262 MESSAGE PASSING
public final Object receive() { synchronized(receiving) {
if (receiver == null) // save the first thread to call receive receiver = Thread.currentThread();
// if currentThread() is not first thread to call receive, throw an exception if (Thread.currentThread() != receiver) throw new
InvalidLinkUsage("Attempted to use link with multiple receivers");
Object receivedMessage = null; |
|
sent.P(); |
// wait for the message to be sent |
receivedMessage = message; |
|
received.V(); |
// signal the sender that the message has |
return receivedMessage; |
// been received |
}
}
Listing 5.2 Synchronous receive method for the link and port classes.
public final class asynchMailbox extends channel { private final int capacity = 100;
private Object messages[] = new Object[capacity]; // message buffer private countingSemaphore messageAvailable = new countingSemaphore(0); private countingSemaphore slotAvailable = new
countingSemaphore(capacity);
private binarySemaphore senderMutex = new binarySemaphore(1); private binarySemaphore receiverMutex = new binarySemaphore(1); private int in = 0, out = 0;
public final void send(Object sentMessage) { if (sentMessage == null) {
throw new NullPointerException("null message passed to send()");
}
slotAvailable.P();
senderMutex.P(); messages[in] = sentMessage; in = (in + 1) % capacity; senderMutex.V(); messageAvailable.V();
}
public final void send() {
/* same as send(Object sentMessage) above except that the line
‘‘messages[in] = sentMessage;’’ becomes ‘‘messages[in] = new Object();’’ */
}
Listing 5.3 Asynchronous asynchMailbox class.
CHANNEL OBJECTS |
263 |
public final Object receive() { messageAvailable.P(); receiverMutex.P();
Object receivedMessage = messages[out]; out = (out + 1) % capacity; receiverMutex.V();
slotAvailable.V(); return receivedMessage;
}
}
Listing 5.3 (continued )
Listing 5.4 shows how to use the link class. Producer and Consumer threads exchange messages with a Buffer thread using links deposit and withdraw. The Buffer thread implements a one-slot bounded buffer. The Producer builds a Message object and sends it to the Buffer over the deposit link. The Buffer then sends the Message to the Consumer over the withdraw link.
5.1.2 Channel Objects in C++/Win32
Listing 5.5 shows a C++ version of the synchronous mailbox class. Methods send() and receive() operate on objects of type message ptr <T>. A message ptr
<T> object is a smart pointer. Smart pointers mimic simple pointers by providing pointer operations such as dereferencing (using operator *) and indirection (using operator ->). Smart pointers also manage memory and ownership for their pointed-to objects. We use smart pointers to help manage message objects that are passed between threads.
A message ptr<T> object contains a pointer to a message object of type T. The message ptr <T> class template uses reference counting to manage message objects. Ownership and memory management are handled by maintaining a count of the message ptr objects that point to the same message. Copying a message ptr object adds one to the count. The message is deleted when the count becomes zero. Sending and receiving message ptr <T> objects simulates message passing in Java. Messages are essentially shared by the sending and receiving threads, and messages are deleted automatically when they are no longer being referenced. As in Java, virtually any type T of message object can be used.
A message that contains an integer can be defined as follows:
class Message { public:
int contents;
Message(int contents_) : contents(contents_){} Message(const Message& m) : contents(m.contents) {} Message* clone() const {return new Message(*this);}
};
264 |
MESSAGE PASSING |
public final class boundedBuffer {
public static void main (String args[]) { link deposit = new link();
link withdraw = new link();
Producer producer = new Producer(deposit); Consumer consumer = new Consumer(withdraw); Buffer buffer = new Buffer(deposit,withdraw);
// buffer will be terminated when producer and consumer are finished buffer.setDaemon(true); buffer.start();
producer.start();
consumer.start();
}
}
final class Message { public int number;
Message(int number ) {this.number = number;}
}
final class Producer extends Thread { private link deposit;
public Producer (link deposit) { this.deposit = deposit; } public void run () {
for (int i = 0; i<3; i++) { System.out.println("Produced " + i); deposit.send(new Message(i));
}
}
}
final class Consumer extends Thread { private link withdraw;
public Consumer (link withdraw) { this.withdraw = withdraw; } public void run () {
for (int i = 0; i<3; i++) {
Message m = (Message) withdraw.receive(); // message from Buffer System.out.println("Consumed " + m.number);
}
}
}
final class Buffer extends Thread { private link deposit, withdraw;
public Buffer (link deposit, link withdraw) { this.deposit = deposit; this.withdraw = withdraw; }
Listing 5.4 Java bounded buffer using channels.
