CS452 - Real-Time Programming - Fall 2009

Lecture 31 - CSP, Occam, Go

Reminders


CSP - Communicating Sequential Processes

Overall Context

  1. Collection of sequential processes. Examples
  2. Interprocess communication. Examples

CSP

Based on the concept of a channel

There has to be a way for the two processes to get hold of the same channel. Examples,

When CSP is provided by an operating system type safety is most likely to be provided at run-time.

When CSP is provided by a programming language type safety can often be provided at compile-time.


The Transputer

Use many co-operating, medium capability microCPUs to do a big job.

Problem is communication

Comminication requires either

The transputer was an early, now vanished, example of the latter

Occam 2

Basic idea

  1. processes (tasks)
  2. channels
  3. time

Combining processes

  1. sequential
  2. conditional
  3. looping
  4. parallel
  5. alternation

Channels

  1. input
    channel ? variable
  2. output
    channel ! value // the value of a variable or the result of an procedure
  3. input & output provide synchronization

Time

The Result

You can write a type-safe server, BUT

Program structure is more static than is allowed in your system.

This might be a good thing.


Go

For comprehensive information: Go here.

Implementing data-flow in Go

Go has channels as first class objects, just as Occam does.

  1. Here is an example that puts into a channel the natural numbers 2, 3, ...
  2. ch is a variable of type chan with a `protocol' which is the type int
  3. The <- operator puts i into ch.
    func generate(ch chan int) { 
        for i := 2; ; i++ { 
            ch <- i  // Send 'i to channel 'ch'.
        }         
    }

Here is another example that gets numbers from one channel and sends some of them to another.

  1. The caller of these functions must pass the channels into the functions.
  2. The same operator , <-, gets an int from the channel for the assignment to i.
    func filter(in, out chan int, prime int) {
        for {
            i := <-in;  // Receive value of new variable 'i' from 'in'.
            if i % prime != 0 {
                out <- i  // Send 'i' to channel 'out'.
            }
        }
    }

These two functions should execute simultaneously like processes joined by a Unix pipe.

  1. make instantiates a chan int
  2. go instantiates what they call a goroutine, which eventually computes the sum
  3. result := blocks until sum puts the result into the channel it has been given, ch.
  4. Input from the channel generalizes wait().
    ch := make(chan int);
    go sum(hugeArray, ch);
    // ... do something else for a while
    result := <-ch;  // wait for, and retrieve, result

Putting generate and filter together shows how goroutines get a common channel for communication.

  1. generate is created and will start putting natural numbers into ch.
  2. In the for loop, main grabs the first number from ch (2), and prints it.
  3. It then makes another channel and passes the two channels to filter along with the first prime (2).
  4. filter gets the subsequent numbers from generate and passes those not divisible by 2 to ch1.
  5. ch1 is assigned to ch and the first number out of filter (3) is the new prime.
  6. A second filter goroutine is instantiated removing numbers divisible by 3.
  7. And so on

Note that we are here creating a set of goroutines that gives a data-flow computation.

    func main() {
          ch := make(chan int);  // Create a new channel.
          go generate(ch);  // Start generate() as a goroutine.
          for {
                prime := <-ch;
                fmt.Println(prime);
                ch1 := make(chan int);
                go filter(ch, ch1, prime);
                ch = ch1
          }
    }

Making a server in Go

A server listens for a request then replies with the result.

  1. The reply is handled by having the client give a reply channel as part of the request
        type request struct {
            a, b    int;
            replyc  chan int;
        }
  2. The server sits in a FOREVER loop waiting for input on its service request channel
        func server(service chan *request) {
            for {
                req := <-service;
                go worker(req);  // don't wait for it
           }
        }

    It then creates a worker to do the work, passing on the request including the channel for replying.

  3. The worker does the work
        func worker(req *request) {
             reply := sum(req.a, req.b);
             req.replyc <- reply;
        }

    Requests can be replied to out of order.

  4. Here is a different way to start the server, a function that
        func startServer( ) chan *request {
           req := make(chan *request);
           go server(op, req);
           return req;
        }
  5. And here is the server in action
        func main() {
            adder := startServer( );
            const N = 100;
            var reqs [N]request;
            for i := 0; i < N; i++ {
                req := &reqs[i];
                req.a = i;
                req.b = i + N;
                req.replyc = make(chan int);
                adder <- req;
            }
            for i := N-1; i >= 0; i-- {   // doesn't matter what order
                if <-reqs[i].replyc != N + 2*i {
                    fmt.Println("fail at", i);
                }
            }
            fmt.Println("done");
        }

    Making the client start the server hides a little complexity here.

The description of the Go memory model goes into the details of synchronization: it is educational in the context of cs452.


Return to: