UCLA Parallel Computing Laboratory

Tutorial: Parallel Programming with Maisie


Table of Contents


Introduction

This paper describes the creation of simple parallel programs in Maisie. Those people familiar with the Maisie language can skip over the language description. Maisie has been implemented on a variety of sequential workstations and laptop machines, on networks of workstations and on scalable MPP platforms like the IBM SP2 and on symmetric multiprocessor (SMP) platforms like the SparcStation 1000. It has been used for the parallel simulation of a number of applications in diverse areas including queueing networks, VLSI designs, parallel programs, mobile wireless multimedia networks, ATM networks, and high-speed communication switches and protocols.

Maisie Language

Maisie extends the C language as described here.

Entities

A Maisie program is a collection of entity definitions and C functions. An entity definition (or an entity type) describes a class of objects. An entity instance, henceforth referred to simply as an entity, represents a specific object in the physical system. Every Maisie program must have a driver entity. This entity initiates execution of the program and serves essentially the same purpose as the main function in C.

The declaration of an entity is syntactically similar to that of a C function; the key difference is that unlike a function, an entity does not return a value. An entity type consists of a heading that declares the name and formal parameters of the entity type and a body that declares local variables and the message types that may be received by the entity type and defines its actions. For instance, the entity type declared in Figure 1 describes an object that models a resource manager. This entity type will be used as a running example to illustrate various Maisie constructs. The heading in lines 1 and 2 of the figure indicates that the entity type is called Manager and has one integer parameter called num_resources. The body of the entity is contained within curly braces in lines 3--16. The body is a compound C statement that may include message declarations (described in the next section) together with standard C declarations. The body may also include constructs to send and receive messages as described subseqeuntly.

An entity is created by the execution of a new statement and is automatically assigned a unique id on creation. Maisie defines a type called ename which is used to store entity-identifiers. The following statement will create an instance of entity type Manager and store its identifier in variable manager, which must be declared to be of type ename.

manager = new Manager{10};
An entity can internally refer to its id using the keyword self. An entity instance terminates itself by `falling off the end'. (In the current implementation of the parallel runtime system, the user must explicitly terminate the program with a call to terminate().)

 1 entity Manager {num_resources}
 2   int num_resources;
 3 {
 4   int resources = num_resources;
 5   message Request {ename requester;};
 6   message Release;
 7   for (;;)
 8     wait until {
 9        mtype(Request) st (resources > 0) {
10          resources--;
11          invoke msg.Request.requester with Resource;
12        }
13     or mtype(Release)
14        resources++;
15     }
16 }
Figure 1: A Resource Manager: Single Resource

Messages

Entities communicate with each other using buffered message passing. Every entity has a unique message buffer; asynchronous send and receive primitives are provided to respectively deposit and remove messages from the buffer.

Declaring Message Types

Maisie uses typed messages. An entity type must define the types of messages that may be received by its instances. A message type is similar to a struct in C and consists of a name and a (possibly empty) parameter list. For instance, two message types are defined for the Manager entity type(lines 5--6): Request which has one parameter of type ename, and Release which has no parameters.

Sending a Message

An entity sends a message by executing an invoke statement. The invoke statement has the following form:
invoke destination with message_type;
where destination is a variable of type ename and message_type is a message whose type has been declared by the destination entity destination. For instance, execution of the invoke statement in line 11 of the Manager will deposit a message of type Resource in the message buffer of the entity identified by requester. Message type declarations are necessary only within the entity type of the destination entity (although for software engineering reasons, message types should be declared in a common header file).

Receiving a message

An entity accepts messages from its message-buffer by executing a Maisie wait statement. In its most commonly used version, the wait statement has the following syntactic form:
	wait until {
	   mtype(m_1) [st b_1] statement_1;
	or mtype(m_2) [st b_2] statement_2;
	 .
	 .
	 .
	or mtype(m_n) [st b_n] statement_n};
where m_i is a message-type, b_i an optional boolean expression referred to as a guard, and statement_i is any C or Maisie statement. The guard is a side-effect free boolean expression that may refer to local variables or message parameters. If omitted, the guard is assumed to be the constant true. The message-type and guard are together referred to as a resume condition and the resume condition together with the statement is refered to as a resume statement. A resume condition with message-type m_i and guard b_i is said to be enabled if the message buffer contains a message of type m_i, which if delivered to the entity would cause b_i to evaluate to true; the corresponding message is called an enabling message. A resume condition that is not enabled is said to be disabled.

For instance, the wait statement in line 8 of the Manager entity consists of two resume statements. The resume condition (line 9) in the first statement ensures that the entity accepts a Request message only if enough resources are available (resources > 0). If so, the resource is allocated to the requesting entity (line 10-11). Keyword msg is used to refer to the last message that was removed from its buffer and delivered to the entity. The second resume statement (line 13) accepts a Release message and simply increments the count of available resources. As discussed subsequently, the resume condition in a wait statement may include multiple message-types, each with its own boolean expression. This allows many complex enabling conditions to be expressed directly, without requiring the programmer to describe the buffering explicitly.

If two or more resume conditions in a wait statement are enabled, then, in general, the enabling message which arrived first will be chosen. If the two messages arrived from the same source, then they will arrive in the same order they were sent. Otherwise, no assumption can be made about the order of arrival. By default, if all resume conditions in the wait statement are disabled, the corresponding entity is suspended until it receives an enabling message. A timeout mechanism is also supported as described in the following section.

 1 entity Job{manager}
 2   ename manager;
 3 {
 4   message Resource;
 5   for (;;) {
 6     hold(processing_time);
 7     invoke manager with Request{self};
 8     wait until
 9       mtype(Resource) {
10         /* use the Resource */
11         invoke manager with Release;
12       }
13   }
14 }
Figure 2: A Job Entity

Non-blocking Receive and Timeouts

Maisie also provides a construct for a non-blocking receive, which is specified as follows:
	wait 0 until {
	   mtype(m_1) [st b_1] statement_1;
           ...
        or mtype (timeout) {
	    ... }
	}

If no enabling message is available at the time of this statement's execution, the entity will receive a timeout message. A non-blocking wait statement must contain a resume statement that specifies the action to be executed by the entity on receipt of a timeout message.

Simple Examples

Very Simple

Ping Pong

The ping pong program (written by Monnica Terwilliger) is a simple example of messages being passed back and forth between two entities. This particular program was implemented to test message delivery times on a particular machine.
  #include "maisie.h"

  #define  N     50000    /* N is the number of rounds */
  #define  B      1000    /* B is the message size, in bytes */
  #define  First     1    /* The first player prints the results */
  #define  Second    0    /* The second player doesn't */

  /*---------------------------------------------------
   * Driver entity
   *-------------------------------------------------*/
  entity driver {}
  {
    ename Jack, Jill;

    Jack = new Player{First};
    Jill = new Player{Second};

    /* Send opponent's name to each entity */

    invoke Jack with Opponent{Jill};
    invoke Jill with Opponent{Jack};

    /* Start the game. */

    invoke Jack with Ball{'0'};
  }

  /*---------------------------------------------------
   * Player entity
   *-------------------------------------------------*/
  entity Player {i_am_first}
    int i_am_first;
  {
    int   trips;
    ename opponent;

    message Opponent {ename name;} opp;
    message Ball {char size[B];}   ball;

    /* Wait to receive opponent's name. */

    wait until mtype (Opponent)
      opponent = msg.Opponent.name;

    /* Wait for the ball, and then hit it back to opponent. */

    for (trips = 1; trips < N; trips++)
      wait until mtype(Ball)
        invoke opponent with Ball{'0'};

    /* Game over */

    if (i_am_first) {
      printf("Game over after %d rounds and %d byte ball size!", N, B);
      wait until mtype(Ball)
        terminate();
    }
  }
Figure 3: Ping Pong

Sieve of Eratosthenes

This example programs the Sieve of Eratosthenes to generate all prime numbers less than 1000. The first instance of the Sieve entity is created by the driver and subsequent instances are created recursively such that the first number received by a new instance is a prime number. Subsequently the entity discards all multiples of its prime number and sends others to the next sieve in the pipeline. Figure 4 presents a sequential implementation of the program.
1  #include "maisie.h" /* formerly mayc.h */
2
3  entity Sieve{myno}
4     int myno;
5  {
6     message Number {int number;} number;
7     ename next_sieve;
8     int myprime;
9
10    wait until mtype(Number)
11         myprime = msg.Number.number;
12
13    printf("Sieve number %d is for prime number %d\n", myno, myprime);
14
15    next_sieve = new Sieve{myno + 1};
16
17    for (;;)
18       wait until mtype(Number)
19            if(msg.Number.number % myprime)
20              invoke next_sieve with Number = msg.Number;
21 }
22
23 entity driver{}
24 {
25    int   i;
26    ename first;
27    first = new Sieve{1};
28    for (i = 2 ; i <= 1000; ++i)
29       invoke first with Number{i};
30 }
Figure 4: Sequential Sieve of Eratosthenes

The sequential program may be refined for a parallel implementation simply by modifying the new statements to create subsequent sieve entities on remote nodes. Assuming that the parallel implementation will be executed on N nodes, each entity creates the next sieve entity on a neighboring node. Required modifications to the preceding program are indicated in Figure 5. The parallel version may also be compiled and executed on a sequential architecture.

15    next_sieve = new Sieve{myno + 1} at (myno + 1);
   ...
27    first = new Sieve{1} at 1;
Figure 5: Parallel Sieve of Eratosthenes

Scientific Computation

Graph Algorithms

All-Pairs Shortest Path

This program (written by Mike Patterson) implements a parallel version of the Floyd-Warshall algorithm for solving the all-pairs shortest path problem in a directed graph. The digraph is represented as a matrix Graph[SIZE][SIZE] of edge weights. For simplicity, each node is considered to be self-connected with an edge of weight 0, -1 is used to represent a non-existent edge, and all existing edges have positive weight.

The sequential Floyd-Warshall algorithm works as follows:

  for k = 0 to SIZE - 1
    for i = 0 to SIZE - 1
      for j = 0 to SIZE - 1
        Graph[i][j] = min(Graph[i][j], Graph[i][k] + Graph[k][j]);
with additional checks for non-existent edges.

The parallel version (source code), assigns each row of the Graph matrix to an entity. At the kth iteration, the row-k entity broadcasts its row to all the other row entities and each row executes the j loop shown above.

Distributed algorithms

Dining Philosophers


Execution of Maisie Programs

Maisie programs are executed just as normal C programs are executed. Please see the Maisie User's Manual for details on compilation and execution of Maisie Programs.

Advanced Maisie Facilities

Wait Statement

The most common form of the wait statement was described earlier. In this section we describe extensions that can be used to specify more complex resume conditions.

Message Parameters

The guard in a resume statement can reference both local variables of the entity and message parameters. For instance, assume that the manager in our running example receives requests for one or more printer units and that incoming requests are serviced using the first-fit discipline. The following fragment shows how the resume condition is modified to ensure that a Request message is accepted only if the requested number of units are available.

	message Request {ename requester; int count; };
	 .
	 .
	 .
        wait until {
          mtype(Request) st (msg.Request.count <= resources)
        .
        .
        .

Maisie also provides a number of pre-defined functions that may be used by an entity to inspect its message buffer. For instance, the function qsize(m_t) returns the number of messages of type m_t in the buffer. A special form of this function called qempty(m_t) is defined, which returns true if the buffer does not contain any messages of type m_t, and returns false otherwise. This function may be used to impose priorities on incoming messages. Assume that the manager is to be modified such that a request message is accepted only if no Release messages are present in the buffer. A simple way to do this is to strengthen the guard for the Request message as follows:

	wait until {
	  mtype(Request) st (qempty(Release) && (msg.Request.count <= resources)) 

Rankers

The ranker facility gives the user the ability to select a message to process by comparing some parameter value of all available messages. If the message buffer contains exactly one enabling message, then that message is delivered to the entity. If the buffer contains more than one enabling message of a given type, the ranker is used to select a unique enabling message: if keyword max (min) is used, the enabling message with the largest (smallest) value for parameter is delivered to the entity.

The following example (taken from the Maisie User's Manual) illustrates simple wait statements with a single resume statement. The resume condition in the first wait statement is enabled if the message-buffer contains a req message. The resume condition in the second wait statement is enabled only if the buffer contains a req message whose parameter units is not greater than the entity variable units. Note, since the value of the mvar oldreq is not modified until an enabling message is received, msg must be used in the guard to reference the message being inspected. The last example causes req messages to be sorted and received in increasing order of the message parameter units.

	wait until mtype (req);
	wait until oldreq = mtype (req)
		st (msg.req.units <= units);		/* First-Fit */
	wait until oldreq = mtype (req) min units
		st (msg.req.units <= units);		/* Least-Fit */

Compound Resume

At times, we would like to not process an event until two or more enabling conditions are met. For example, in the Dining Philosophers problem, the Philosopher cannot eat until he acquires two forks. This can be specified in Maisie using a compound resume, in which the keyword and is used to conjoin multiple resume conditions. The following is a generic example of its use.
	wait until
	    mvara = mtype (ma) [max va] [st ba]
	and mvarb = mtype (mb) [max vb] [st bb]
	and mvarn = mtype (mn) [max vn] [st bn]
	{
	  statement;
	}

Friend Functions

Friend functions may very well be removed from the next release of the Maisie compiler. To ensure compatibility, we recommend that you not use them.

Acknowledgements

This tutorial borrows heavily from the Maisie User's Manual written by Wen-Toh Liao).

The Ping Pong program was written by Monnica Terwilliger.

The All Pairs Shortest Path program was written by Mike Patterson and modified by Richard Meyer to conform to the style of the document.


Written by Rajive Bagrodia and Richard Meyer.
Last updated: Friday, 06-Oct-2000 20:03:26 PDT