CS 334
Programming Languages
Spring 2002

Lecture 22


More OO Languages and flexibility

Did any of you write your interpreter by using inheritance to extend classes of PrettyPrinter? Why was that hard?

Because of the problems, instead modified the interfaces and classes (or started over). Violates "Open-closed" principle of classes. Classes should be open to extensions, but closed to modifications of the class itself. Problems with modification include impact on subclasses, users, etc. Can't always avoid this, but it is desirable if possible.

A possible solution to these problems is to use the Visitor pattern when dealing with heterogeneous data structures. An example of the use of the Visitor pattern (where interface LangProcessor represents the Visitor) can be found in the collection of GJ classes and interfaces here). The pattern is not an easy one to understand at first, but if you followed the explanation in class, hopefully you see its flexibility in allowing one to add new operations to a set of classes by simply adding a new visitor. [Note that I have made instance variables accessible in order to keep the code short. It is NOT good style! We can also get rid of the "instanceof" expression and cast in Interp by adding a getClosure method to the Val interface.]

Notice that there are problems with the visitor pattern. While it is easy to add new operations (e.g., a new class implementing LangProcessor), it is not easy to add new expressions/formulas. This has been a challenge for software engineers and language designers, and several papers have been written about it. The MyType construct along with exact types can handle it, though the types in the code are a bit more complex, and all of the term classes need another type parameter corresponding to an extension of LangProcessor.

CONCURRENT PROGRAMMING

Classification of machine architectures:

S = single, M = multiple, I = instruction, D = data

SISD - traditional von Neumann - one processor, one piece of data at a time,

SIMD - currently used supercomputers (typically synchronous)

pipeline (interleave operations) for speed-up or

array-processor - do same thing to all elts of array,

MIMD - most interesting to computer science (asynchronous).

1. Tightly coupled (shared memory)

Difficulties - synchronization - wait for one to finish for another to take output and start.

Generalization of sequential algorithm not necessarily fastest on parallel machine

Contention over resources - reader - writer problem.

2. Distributed computing system (typically no global memory)

Each processor has its own memory but share a common communications channel.

Processes

Process: instance of program or program part that has been scheduled for independent execution.

In one of three states: executing, blocked, or delayed (waiting).

Problems:

  1. Synchronizing w/ other processes.

  2. Communicate data.

With shared memory, communication typically via shared memory.

Problem: Mutual exclusion (reader-writer contention)

Distributed memory, communication via sending messages:

Problem: How to asynchronously send and receive messages?

Need mechanisms in OS to

  1. Create and destroy processes.

  2. Manage processes by scheduling on one or more processors.

  3. Implement mutual exclusion (for shared memory).

  4. Create and maintain communication channels between processors (for distributed memory).

Language mechanisms supporting concurrency

Can't deliver detailed description of how to support concurrency here. Instead focus on language mechanisms to support concurrency.

Three major mechanisms:

  1. Semaphores (for mutual exclusion)

  2. Monitors (for mutual exclusion)

  3. Message passing (using "tasks")

Focus here on producer/consumer or bounded buffer problem.

Two processes cooperating, one by adding items to a buffer, the other removing items. Ensure not remove when nothing there and not overflow buffer as well.

Text also focuses on parallel matrix multiplication (read on own).

Text also discusses some ways of handling with simple extensions of existing languages:

Coroutines also worth noting (part of Modula-2). Iterators were a special case.

Idea is co-equal routines which pass control back and forth.

E.g., Modula-2 has library supporting routines:

NewCoroutine(P: PROC; workspaceSize:CARDINAL; VAR q: COROUTINE);

Starts up new coroutine, q, by executing procedure P.

Transfer(VAR from, to: COROUTINE)

Transfers control from one coroutine to another.

Can have multiple coroutines executing same procedure or can all be distinct.

Usually run on single processor.

Can think of as supporting multi-tasking. Good for writing operating systems.

Semaphores

Support mutual exclusion and synchronization in shared-memory model.

Three operations:

    InitSem(S: Semaphore; value: integer);

    Delay(S: Semaphore);

    Signal(S: Semaphore);

InitSemstarts up semaphore with an initial (non-negative) value.

Delay(S): If S > 0 then S := S - 1 else suspend self

Signal(S): if processes are delayed, then wake up a process, else S := S + 1;

Think of Delay(S) as claiming a resource so that no one else can get it, while Signal(S) releases the resource.

In order to solve mutual exclusion problem, must ensure that Delay and Signalexecute atomically (i.e., cannot be interrupted and no one else can execute at same time).

If start w/ S = 1 then protect a critical region by:

    Delay(S);    -- grab token
    {Critical region}
    Signal(S);  -- release token
Can also start with other values of S, e.g., if start w/S = 0 and call Delay(S) then suspend execution until another process executes Signal(S).

Solution to bounded buffer:

Book shows solution in Java -- we'll do solution in procedural pseudo-code for variety.

Suppose also have procedures:

    CreateProcess(p:PROC; workspacesize: CARDINAL);
        Creates nameless process
    StartProcesses;  -- starts all processes which have been created.
    Terminate;  -- stop execution of process

Also presume exists Buffer[1..MaxBuffSize]

When all processes are terminated control returns to unit calling StartProcesses.

Main program:
    CreateProcess(Producer,WorkSize);   -   - create at least one producer
    CreateProcess(Consumer,WorkSize);   -- create at least one consumer
    BufferStart := 1;  BufferEnd := 0
    InitSem(NonEmpty, 0)    -- semaphore w/initial value of  0 to indicate empty
    InitSem(NonFull, MaxBuffSize)   -- semaphore w/initial value of  size of buffer
    InitSem(MutEx,1)        -- semaphore used for mutual exclusion
    StartProcesses
end;

Procedure Producer;
begin
    loop
        read(ch)
        Delay(NonFull);
        Delay(MutEx);
        BufferEnd := BufferEnd MOD MaxBuffSize + 1;
        Buffer[BufferEnd] := ch;
        Signal(MutEx);
        Signal(NonEmpty);
    end loop;
end;

Procedure Consumer;
begin
    loop
        Delay(NonEmpty);
        Delay(MutEx);
        ch := Buffer[BufferStart];
        BufferStart := BufferStart MOD MaxBuffSize + 1;
        Signal(MutEx);
        Signal(NonFull);
        Write(ch)
    end loop
end;

Why is there a MutEx semaphore?

Technically it is not necessary here since Producer only changes BufferEnd, while Consumer only changes BufferStart, but if they both changed a count of the number of items in the buffer [as the example in the book does!] would be important to keep them from executing at the same time!

What would go wrong if you changed the order of the two Delay's at the beginning of either Producer or Consumer?

Biggest problem with semaphores is that they are too low level and unstructured. Accidentally reversing order or Delay'ing twice could be disastrous.

Monitors

Monitors are a much higher-level construct to support mutual exclusion and synchronization.

The idea is to provide an ADT with "condition variables", each of which has an associated queue of processes and suspend (or delay) and continue ops for en and dequeuing processes. Suspend always stops current, continue starts up new if any suspended or waiting.

Concurrent Pascal uses monitors. Java "synchronized" similar. (Book explains Ada 95 protected types, which are essentially monitors.)

type buffer = monitor;

var store: array[1..MaxBuffSize] of char;
    BufferStart, BufferEnd, BufferSize: integer
    nonfull, nonempty: queue;

procedure entry insert(ch: char);
begin
    if BufferSize = MaxBuffSize then suspend(nonfull);
    BufferEnd :=BufferEnd mod MaxBuffSize + 1;
    store[BufferEnd] := ch;
    BufferSize := BufferSize + 1;
    continue(nonempty)
end;

procedure entry delete(var ch: char);
begin
   if BufferSize = 0 then suspend(nonempty);
   ch := store[BufferStart];
   BufferStart := BufferStart mod MaxBuffSize + 1;
   BufferSize := BufferSize -1;
   continue(nonfull);
end;

begin (* initialization *)
   BufferEnd := 0;
   BufferStart := 1;
   BufferSize := 0
end;

type producer = process (b: buffer);
var ch: char;
begin
    while true do begin
        read(ch);
        b.insert(ch)
    end;
end

type consumer = process(b: buffer);
var ch: char;
begin
    while true do begin
        b.delete(ch);
        write(ch)
    end
end;

var p: producer; q: consumer; b:buffer; 
begin 
   init b, 
   p(b), 
   q(b) 
end.

Notice improved structure!


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