CS 334
Programming Languages
Spring 2002

Lecture 23


Message Passing

With distributed systems, cannot rely on shared variables for synchronization and communication.

With message passing, rely on send and receive operations:

    Procedure Send(To: Process; M: Message);
    Procedure Receive(From: Process; M: Message);
Variants exist in which drop "To" and "From" fields (broadcast messages).

Synchronization questions:

If wait for each other, called "rendezvous". Owise often use a mailbox to store messages.

Ada's tasks combine features of monitors and message passing.

Process in Ada is called a task (defined much like a package).

Task exports entry names to world (like a monitor) which can be "called".

Task accepts an entry call via "select " statement (not name sender).

Select allows choice of which accept is selected.

Syntax:

    select
      [when <cond> =>] <select alternative>
      {or [when <cond> =>] <select alternative>}
      [else <statements>]
    end select
when statements form guards.

System determines which "when" conditions are true and select one which has an "accept" process waiting. If several than choose arbitrary one.

Caller is blocked only during body of "accept" clause.

Can have other statements outside the "accept".

Ex from Ada:

task Buffer is      -- specification
    entry insert(ch: in CHARACTER);
    entry delete(ch: out CHARACTER);
    entry more(notEmpty: out BOOLEAN)
end;

task body Buffer is
    MaxBufferSize: constant INTEGER := 50;
    Store: array(1..MaxBufferSize) of CHARACTER;
    BufferStart: INTEGER := 1;
    BufferEnd: INTEGER := 0;
    BufferSize: INTEGER := 0;
begin
    loop
        select
            when BufferSize < MaxBufferSize =>
                accept insert(ch: in CHARACTER) do
                    BufferEnd := BufferEnd mod MaxBufferSize + 1;
                    Store(BufferEnd) := ch;
                end insert;
                BufferSize := BufferSize + 1;
            or when BufferSize > 0 =>
                accept delete(ch: out CHARACTER) do
                    ch := Store(BufferStart);
                end delete;
                BufferStart := BufferStart mod MaxBufferSize + 1;
                BufferSize := BufferSize -1;
            or
                accept more (notEmpty: out BOOLEAN) do
                    notEmpty := BufferSize > 0;
                end more;
            or
                terminate;
        end select;
    end loop
end Buffer;

task type Producer;
task body Producer is
    ch: CHARACTER;
begin
    loop
        TEXT_IO.GET(ch);
        Buffer.insert(ch);
    end loop;
end Producer;

task type Consumer;
task body Consumeris
    ch: CHARACTER;
begin
    loop
        Buffer.delete(ch);
        TEXT_IO.PUT(ch);
    end loop;
end Consumer;

Note Producer/Consumer run forever - no entries.

Exit from task when at end and no open subprocesses or waiting with "terminate" alternative open and parent has executed to completion. If this true of all children of process then all terminate simultaneously.

Java concurrency

In Java, concurrency is supported via threads -- objects of class Thread. Java classes extending Thread merely have to have a constructor and a run method (though other methods may be defined).

The thread begins execution when a start message is sent to it (typically by the constructor, but it could be sent from another object). The start method does some bookkeeping and then calls the run method. When the run method terminates, the thread dies. Thus a long-lived thread typically includes a while loop in the run method.

   class SomeThread extends Thread {
      public SomeThread(...) {...; start();}

      public void run() {...}
}

Alternatively, threads can be designed by writing a class that implements interface Runnable (which requires that the class implement a run method. This is handy when the class already extends another class (recall there is no multiple inheritance in Java).

   class SomeRunnable implements Runnable {
      public SomeRunnable(...) {...}

      public void run() {...}
   }

A new object of this class can then be passed as a parameter to a Thread constructor in order to get a thread object that can be started.

   Thread t = new Thread(new SomeRunnable(...));
   t.start();

One method can be made to wait for another by sending the join message to the second. I.e., executing t.join() causes the thread currently executing to wait for thread t to terminate before resuming execution

It is important to remember that threads are not tied to objects. Thus if thread t is executing the run method of SomeThread, and that method sends a message to a different object, then thread t will execute the corresponding method on that other object.

CS 134 now includes the study of ActiveObject's, which are slightly disguised Java threads. We also provide a pause method, which is similar to Thread's sleep method, except that it does not throw an InterruptedException.

In Java, programmers are allowed to associate locks with methods. Any object may be used as a lock, as follows:

public ... m(...) {
   ...
   synchronized(someObj) { // code in critical section }
   ...
}

The idea is that for each lock, only one block of synchronized code can be executing at a time. For example, suppose two blocks, b1 and b2 are guarded by the same lock. Then if b1 is executing in one thread, and the block b2 is ready to execute in another, b2 will be blocked, until b1 finishes.

Typically, the entire body of a method is guarded. In this case we may write:

public synchronized ... m(...) {
   ...
}

This is exactly equivalent to

public synchronized ... m(...) {
   synchronized(this) {...}
}

The wait() method can be used to suspend the execution of a critical section if it is necessary to wait for some condition to be true. When the wait method has been executed, the thread is put on a wait list associated with its lock. The first object on the list can be awakened by having an object in the lock's critical section send a notify() method. However, this may not be optimal, if that object is still not ready to execute (because the condition may still be false). Hence, it is more common to send a notifyAll() method that awakens all of those in the queue. The bounded buffer example in Java provides a good example of this.

class Buffer {
   private final char[] buf;
   private int start = -1;
   private int end = -1;
   private int size = 0;

   public Buffer(int length) {
      buf = new char[length];
   }

   public boolean more() {
      return size > 0;
   }

   public synchronized void put (char ch) {
      try {
         while (size == buf.length) wait();
         end = (end + 1) % buf.length;
         buf[end] = ch;
         size++;
         notifyAll();
      } catch (InterruptedException e) {
         Thread.currentThread().interrupt(); 
      }
   }

   public synchronized char get() {
      try {
         while (size == 0) wait();
         start = (start+1) % buf.length;
         char ch = buf[start];
         size--;
         notifyAll();
         return ch;
      } catch (InterruptedException e) {
         Thread.currentThread().interrupt(); 
      }
      return 0;
   }
}

class Consumer implements Runnable {
   private final Buffer buffer;

   public Consumer(Buffer b) {
      buffer = b;
   }

   public void run() {
      while (!Thread.currentThread().isInterrupted()) {
         char c = buffer.get();
         System.out.print(c);
      }
   }
}

import java.io.*;

class Producer implements Runnable {
   private final Buffer buffer;
   private final InputStreamReader in = new InputStreamReader(System.in);

   public Producer(Buffer b) {
      buffer = b;
   }

   public void run() {
      try {
         while (!Thread.currentThread().isInterrupted()) {
            int c = in.read();
            if (c == -1) break; // -1 is eof
            buffer.put((char)c);
         }
      } catch (IOException e) {}
   }
}

class BoundedBuffer {
   public static void main(String[] args) {
      Buffer buffer = new Buffer(5);  // buffer has size 5
      Producer prod = new Producer(buffer);
      Consumer cons = new Consumer(buffer);
      Thread read = new Thread(prod);
      Thread write = new Thread(cons);
      read.start();
      write.start();
      try {
         read.join();
         write.interrupt();
      } catch (InterruptedException e) {}
   }
}

Brinch Hansen - designer w/Hoare of Monitors hates Java concurrency!

Because it doesn't require programmer to have all methods synchronized, and can leave instance variables accessible w/out going through synchronized methods, it is easy to mess up access w/concurrent programs. Felt that should have had a monitor class that would only allow synchronized methods.

Comparing semaphores and monitors and tasking:

Semaphores very low level.

Monitors are passive regions encapsulating resources to be shared (mutual exclusion). Cooperation enforced by delay and continue statements.

Everything active in Ada tasks (resources and processes)

Monitors and processes can easily be structured as Ada tasks and vice-versa.

Ada tasks better represents behavior of distributed system.


Back to:
  • CS 334 home page
  • Kim Bruce's home page
  • CS Department home page
  • kim@cs.williams.edu