Pages

Monday, December 5, 2016

Mel Writer Zones

Welcome » NERWous C » Mel
  1. Writer Zones
  2. Checkout Operation
  3. Writer OR Zones
  4. Writer LIST Zones
  5. Writer AND Zones
  6. Writer Zone Attributes


Writer Zones

While the reader zone from previous chapters deal with the reader's end of a mel buffer, we now introduce the mel writer zones for exclusive access to the writer's end of the mel buffer. We are using the term "mel buffer" even if the mel variable is not buffered (which is the default of a mel declaration) -- an unbuffered mel is a mel with a buffer of one slot.

Let's create a new task called Generator that wants exclusive access to the mel buffer to Generate and deposit new products. We still have our customary Producer task that has been generating "mainstream" products. This Generator task is used to insert some "rogue" products into the production line.
/* Generator - VERSION 1 */
void Generator(<mel> int store) {
    try {
      <?writer>store {
          store = Generate();
      }
    }
    catch ( store<CLOSED> ) {}
}
A writer zone is differentiated from a reader zone by the presence of the mode writer. While the mode reader is optional and almost never specified for a reader zone due to its being the default mode, the mode writer is required to mark the entrance of a writer zone.

Process

The following behind-the-scene process occurs:
  1. The task puts itself in the mel writers' queue.
     
  2. In due course, the task moves up to the top position of the writers' queue. If the mel buffer is all full, the task will wait at this position, until an empty slot in the buffer opens up. Other writer tasks that come up later have to queue behind the task.
     
  3. If and when an empty slot is available, the task holds on to this slot, gets into the exclusive writer zone and does its Generate work. Note that this exclusivity is for writing only. If the mel buffer has values in the slots at the reading end, consumer tasks can still empty them using the normal mel read access. The task only holds off other writer tasks at the writing end -- which means that other writers must queue behind the task when it is in the writer zone, even if there are available empty mel buffers for them to use.
     
  4. When inside the writer zone, the task makes use of the store variable. This is an eponymous local variable whose initial value is undefined. The usual goal of a task inside the writer zone is to give a relevant value to the eponymous local variable.
     
  5. When the task is done with the writer zone, it either falls out of the writer zone's code at the closing }, or invokes the checkout operation. The default behavior in both cases is to copy the current value of the eponymous variable into the mel buffer slot that the task is holding. The filled slot is then freed for a reader task.

    If the task has not assigned a value to the eponymous variable, the skip behavior takes over. The held mel buffer slot remains empty, and is freed for a new writer task to deposit a value.
     
  6. The task removes itself from the mel writers' queue. Any writer task waiting on one of the writers' queues can now vie for the next empty slot.
Conditions

Let us repeat the conditions that allow the task to get into the writer zone:
  1. The task must be at the top position of the writers' mel queue
  2. The mel buffer must have an empty slot for the task to deposit a potential product
If one of the above conditions is not met, the task will be waiting in the writers' mel queue.

Usage

The Generator example can be written using a straight mel write instead of a mel write zone:
/* Generator - VERSION 2 */
void Generator(<mel> int store) {
    try {
      <?>store = Generate();
    }
    catch ( store<CLOSED> ) {}
}
In VERSION 2, the Generate functionality is done without holding up the mel store. Other writer tasks are free to write to the mel store while Generate is running. Exclusive access to the writer's end is requested only to transfer the Generate'd value to the mel variable. In VERSION 1, the Generate functionality is done inside the exclusive write zone, blocking other writer tasks to write to the mel store even if there are empty slots to receive their products. Which construct to use depends on the logic of the program.


Checkout Operation

A task can get off a writer zone by either falling off the closing } parenthesis or by explicitly invoking the checkout operation. The following version of the Generator is logically equivalent to VERSION 1:
/* Generator - VERSION 3 */
void Generator(<mel> int store) {
    try {
      <?writer>store {
          store = Generate();
          <checkout>;
      }
    }
    catch ( store<CLOSED> ) {}
}
The checkout operator, invoked without any attribute, implements the default behavior, which is for the task to copy the value of the eponymous variable into the top slot at the writer's end of the mel buffer, release that slot for a reader task, remove itself from the mel writer's queue and get off the exclusive writer zone.

Skip Behavior

There are times the logic dictates that copying the value of the eponymous variable to the mel slot is not desired. For example, the Generator task may not want to write to the mel if the Generated value is 0:
/* Generator - VERSION 4 */
void Generator(<mel> int store) {
    try {
      <?writer>store {
          store = Generate();
          if ( ! store ) <checkout skip>;
      }
    }
    catch ( store<CLOSED> ) {}
}
The <checkout skip> causes the task to check out of the writer zone; however the skip attribute prevents the value of the eponymous variable to be copied into the mel slot. The slot remains empty, and is available for the next writer to fill.

The skip behavior can also be used when the eponymous variable is not even present inside the writer zone. Let's introduce a new task called Maintenance whose purpose is the hold off the production line to do some maintenance:
/* Maintenance - VERSION 1 */
void Maintenance(<mel> int store) {
    try {
      <?writer>store {
          doMaintenance();
          <checkout skip>;
      }
    }
    catch ( store<CLOSED> ) {}
}
When Maintenane is in the writer zone, it locks the mel buffer at the writer's end, preventing any writer task to deposit a new product. These writer tasks can only resume depositing after Maintenance has checked out.

The skip attribute is used here to explicitly indicate that the mel buffer at the writer's end will not be updated. However for Maintenance, the skip attribute can be skipped (pun intended) since Maintenance does not update the eponymous variable store (it does not even make a reference to it!). When the eponymous variable is not initialized, the default behavior of checkout is skip:
/* Maintenance - VERSION 2 */
void Maintenance(<mel> int store) {
    try {
      <?writer>store {
          doMaintenance();
          <checkout>;
      }
    }
    catch ( store<CLOSED> ) {}
}
VERSION 2 behaves the same as VERSION 1. It is less verbose than VERSION 1 and also less explicit. Having the skip attribute visible as in VERSION 1 makes it clear the intended behavior of a checkout operation.

There is a third way to write the above Maintenance code. It is to use the mode attribute to specify the skip behavior as the default behavior.

Eponymous Local Variable

As introduced in Process, the eponymous local variable store has the same name as the mel variable store. Its initial value is undefined. The task then works with this local variable within the writer zone. On exit of the zone, if the eponymous local variable has a bona-fide value, this value will fill the mel buffer slot that the task is holding in the zone. This slot will be available for a reader task to empty. If the eponymous local variable remains undefined, then the task will release the holding empty mel buffer slot without filling it. The next incoming writer task can use that slot to deposit its value.

The eponymous local variable is not the same as the locally cached <value> property. The latter contains the mel value from the last time the task successfully reads from the mel. On the other hand, the eponymous local variable starts out as undefined, and is initialized inside the writer zone. If desired, the programmer can initialize the eponymous local variable with the mel <value> property:
store = store<value>;

Waiting Tasks Wake-Up Order

The previous discussion mentions mel buffers when referring to mel variables. To increase concurrency, a mel variable should be buffered, so that reading can be done on one end of the buffer and writing can be concurrently done at the other end. However by default, a NERWous C mel variable is not buffered; or looking in another way, it has a buffer of one slot.

When the task gets into the writer zone of an unbuffered mel variable, it not only holds off other writer tasks waiting to write, it also holds off any reader task waiting to read due to the mel variable being empty. If the task checks out of the writer zone with a value for the mel variable, the waiting reader task will run first to retrieve the newly deposited value and make the mel variable empty again. The writer task waiting at the top of the writers' queue is run next to deposit a new value. However if the task checks out of the writer zone without depositing a value to the unbuffered mel variable, the writer task at the top of the writers' queue is run first to deposit a value before the waiting reader task can run and empty it.


Writer OR Zones

We have seen reader OR zones. Now let's take a look on writer OR zones. Let's modify Generator to make use of two mel variables, and fill the first one that is available.
/* Generator - VERSION 5 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer as=store> (store1 || store2) {
          store = GenerateWithSeed(store<value>);
      }
    }
    catch ( store1<CLOSED> && store2<CLOSED> ) {}
}
This new Generator also makes use of the <value> property. It generates a new value for the chosen mel variable based on the last read value of that mel variable by the task.

Process

This is the behind-the-scene process:
  1. The task puts itself in store1 mel writers' queue. If it finds itself at the top position of store1 writers' queue and that mel buffer has an empty slot, the task holds on to this slot, gets into the writer zone with store1.
     
  2. If one of the above conditions for store1 is not met, the task puts itself in store2 mel writers' queue. If it finds itself at the top position of store2 writers' queue and that mel buffer has an empty slot, the task holds on to this slot, gets into the writer zone with store2.
     
  3. Otherwise, the task will wait on both writers' queues for the writer zone conditions to happen in one of the queues.
     
  4. On the first queue that satisfies the conditions (task first on the queue, mel buffer with empty slot), the task holds on to the found slot, removes itself from the other queue, and gets into the writer zone with that mel variable.
     
  5. When inside the writer zone, the task makes use of the store eponymous local variable, which can represent either store1 or store2 depending on which mel variable has been picked. The name "store" comes from the attribute as=store in the mel writer zone wait construct.
     
  6. The task also makes use of store<value> which is a totally different entity than the eponymous local variable store. The entity store<value> is the local <value> property that the task has cached from its last access from the reader's side of store1 or store2 (depending on which mel variable is picked). This value may be undefined if the task has never successfully read from that mel variable. The Generator version above assumes that GenerateWithSeed will know how to handle undefined arguments.
     
  7. When the task is done (at the closing } of the writer zone), if the task has a non-null value in the eponymous local variable store, this value is inserted into the mel buffer slot that the task is holding on. If GenerateWithSeed returns a null value to the eponymous variable, the eponymous variable is deemed undefined and the task simply releases the hold to the mel buffer slot without updating it. The task remembers which writers' queue it uses, and so removes itself from that queue.

As Attribute

The as attribute is usually needed for an OR list so that it can represent the mel variable that is selected on entry of the writer OR zone, as we see in the Generator example above.

Sometimes, the as attribute is not needed. Let's modify the Maintenance example to use writer OR zone:
/* Maintenance - VERSION 3 */
void Maintenance(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1 || store2) {
          doMaintenance();
      }
    }
    catch ( store1<CLOSED> && store2<CLOSED> ) {}
}
Since Maintenance does not make use of the eponymous local variable, the attribute as is not necessary in the mel zone wait statement.

Traverse Behavior

The behind-the-scene description above uncovers a subtle difference between
<?writer as=store> (store1 || store2)
and
<?writer as=store> (store2 || store1)
The first mel item in the OR list is checked first. If it is emptied constantly by a reader task, the mel write wait on that first item is likely successful and the writer OR zone is spent more with the first mel than with the second mel.

This issue can be resolved by using the random traverse behavior for OR writes:
/* Generator - VERSION 6 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer as=store> (store1 ||<random> store2) {
          store = GenerateWithSeed(store<value>);
      }
    }
    catch ( store1<CLOSED> && store2<CLOSED> ) {}
}


Writer LIST Zones

Unlike the writer OR zone which only requires either specified mel resource, the writer LIST zone allows the programmer to have a task entering a writer zone with all the requested mel resources.

Let us write a new version of Generator using a LIST wait, with a comma separated the mel variables in the wait statement:
/* Generator - VERSION 7 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1, store2) {
          store1 = GenerateWithSeed(store1<value>);
          store2 = GenerateWithSeed(store2<value>);
      }
    }
    catch ( store1<CLOSED> || store2<CLOSED> ) {}
}
This is the behind-the-scene process:
  1. The task puts itself in both the store1 and store2 mel writers' queues.
     
  2. In each queue, whenever the task finds itself at the top position and that mel buffer has an empty slot, the task holds on to this slot, and continues to check for the same write conditions on the other queue.
     
  3. When a task holds on to a slot in a queue, it will prevent other writer tasks waiting on that queue to proceed, even if there are empty slots to accomodate those writers. Since it is also waiting on another queue, a deadlock event can occur if multiple writer tasks vie for the same mel resources in a round-robin fashion.
     
  4. When the write conditions are also satisfied on the second queue, the task holds on the this slot. The task now holds on to both required slots, it can now check in to the writer zone.
     
  5. When inside the writer zone, the task makes use of the store1 and store2 eponymous local variables. These local variables start out as undefined, and then initialized with the result of the function call to GenerateWithSeed.
     
  6. The task also makes use of store1<value> and store2<value>, which are totally different entities than the eponymous local variable store1 and store2. The entity store1<value> and store2<value> are the local <value> properties that the task has cached from its last access to these mel variables from their reader's sides. These values may be undefined if the task has never successfully read from these mel variables. The Generator version above assumes that GenerateWithSeed will know how to handle undefined arguments.
     
  7. When the task is done (at the closing } of the writer zone), if the eponymous variable is non-null, its value is deposited into the corresponding empty slot that the task is holding on. If GenerateWithSeed returns a null value to the eponymous variable, the eponymous variable is deemed undefined and the task simply releases the hold to the mel buffer slot without updating it.

Writer LIST zones can be checked in and out without touching the eponymous local variables, as seen in this version of Maintenance:
/* Maintenance - VERSION 4 */
void Maintenance(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1, store2) { {
          doMaintenance();
      }
    }
    catch ( store1<CLOSED> || store2<CLOSED> ) {}
}
The task Maintenance will enter the writer zone holding on the mel empty slots of both store1 and store2. Upon exit of the zone, the task simply releases the empty slots it is holding without filling them with a value. Waiting or future writer tasks will fill in these slots. As discussed previously, one possible use of this type of Maintenance task is to block write access to the mel product lines in order for some maintenance work to take place.

The writer LIST zone should not be used if the programmer has knowledge of round-robin resources requests. These can cause deadlocks. For example, task 1 has hold to mel 1 and waits for mel 2, while task 2 has hold to mel 2 and waits for mel 1. If deadlocks are possible, writer AND zones should be considered.


Writer AND Zones

The writer AND zone should be used when there is a chance of requests for mel resources to cause deadlocks. The requested resources are listed with the && separator:
/* Generator - VERSION 8 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1 && store2) {{
          store1 = GenerateWithSeed(store1<value>);
          store2 = GenerateWithSeed(store2<value>);
      }
    }
    catch ( store1<CLOSED> || store2<CLOSED> ) {}
}
The writer AND zone is entered via the mel AND write waits applied to all the requested mel variables (store1 and store2 in the above example). This wait is an all-or-nothing wait. If the Generator task has the "top-of-the-queue" condition satisfied in one queue, but not at the other queue(s), it will allow other writer tasks at that queue to "jump the line" and put their products to the mel buffer empty slots. When the task belongs to the "top-of-the-queue" groups in all of the mel requests, then it will prevent later-coming writers to "jump the line". This allows the task to have exclusive access to the writer's end of the mel buffers of all the requested mel variables during its stay in the writer exclusive zone.

Inside the writer zone, the task makes use of the eponymous local variables that represent the requested mel variables. The processing on checkout of a writer AND zone is the same as for a writer LIST zone described previously.

Both the reader AND zone and the writer AND zone prevent deadlocks from multiple tasks that vie for the same resources in a circular way. Is the waiting problem more prevalent with the reader AND zone, since there is only one mel value at the top of the reading side of a mel buffer? At least at the writer's end, we can increase the buffer size so that multiple writers can deposit their products. However the waiting to enter a writer AND zone is as dire as waiting to enter a reader AND zone. This wait is not to access the available empty buffer slots (which we can increase), but to the top end of the writer's queue (which there is only one, like the top end of the reader's end queue). Once a task is in the writer end zone, it will block other writer tasks to access the writer's end of the queue, even though there may be empty slots for them to deposit their products.


Writer Zone Attributes

The attributes for a mel writer zone wait is similar to the ones for a mel reader zone:

AttributeSynopsis
modeDefault mode on checkouts
priorityWait to check in using this priority
timeoutWait to check in up to this timeout value. If the timeout is reached, a <TIMEOUT> exception will be raised.


Mode Attribute

The mode keyword is usually omitted. The following code snippets are equivalent:
<? mode=writer> store { ... }  /* canonical */
<?writer> store { ... }  /* popular */
There are two mode attributes for a mel writer zone wait:

Mode ValueSynopsis
writerAs a required value, it differentiates a mel writer zone wait from a mel reader zone wait. As the default behavior, it indicates that the checkout behavior is to update the held-up mel slot with the initialized value of the eponymous local variable. If the eponymous local variable is not defined or initialized, the checkout behavior is the skip behavior.
writer+skipThis sets the default checkout behavior to be the skip behavior, even if the eponymous local variable is defined and initialized.

The Maintenance example used in introducing the skip behavior on checkout, indicates that any checkout statement should be invoked with the skip attribute. Since the checkout behavior is always a skip, it can be specified on entry of the mel writer zone with the mode attribute:
/* Maintenance - VERSION 5 */
void Maintenance(<mel> int store) {
    try {
      <?writer+skip>store {
          doMaintenance();
          <checkout>;
      }
    }
    catch ( store<CLOSED> ) {}
}



Previous Next Top

Monday, September 5, 2016

Structured Mels

Welcome » NERWous C » Mel
  1. Mel Structures
  2. Mel Structure Buffers
  3. Mel Structures With Multiple Items
  4. Mel Structure Value Property
  5. Mel Structure Direct Itemized Access
  6. Mel Structures In Exclusive Zones
  7. Mel Structures Inside Local Structures


Mel Structures

In the previous section on Mel Arrays, we have the statement:
<mel> int stores[NUMITEMS];
where each element of stores is a mel element by itself and can be accessed independently. In this section, we are going to access stores as a single, monolithic mel element. Under NERWous C, this is done by using the mel struct construct:
#define NUMITEMS 3
struct s { int contents[NUMITEMS]; };
main () {
    <mel> struct s store;
    <!>Producer (store);
    <!>Consumer (store);
}
/* VERSION 1 */
void Producer (<mel> struct s store) {
    int c;
    struct s localstore;
 
    for (int i=0; i<NUMITEMS; ++i) {
       c = produce();
       if ( c == 0 ) {
          <close>store;
          return;
       }
       localstore.contents[i] = c;
     }
     
     <?>store = localstore;
}
/* VERSION 1 */
void Consumer (<mel> struct s store) {
    struct s localstore;
    try {
       localstore = <?>store;
       for (int i=0; i<NUMITEMS; ++i) {
            consume (localstore.contents[i];
       }
    }
    catch ( store<CLOSED> ) {
        printf ("Exit because the Store is closed");
        return;
    }
}
The C language built-in struct construct wraps around the contents array and allows the whole array to be accessed as a single mel element. Declaring a mel struct variable is a two-step process. First we define a regular C language struct template. Then we append the <mel> construct in order to declare the struct entity as a mel variable.

The Producer task uses a mel write to update the structured mel store. First it builds a local copy of store by running a serial for loop to populate the array contents with NUMITEMS items. If it detects a badly produce'd item, it prematurely closes the mel store. If all the products are good, it sends the whole local copy to the remote mel store, via the statement <?>store = localstore.

The Consumer task does a mel wait on the structured mel store. If during this wait it gets a CLOSED exception due to the Producer's premature closure, it jumps to the catch code block. When it sees that Producer has updated the mel store, it retrieves that value into its local copy via the mel read operation: localstore = <?>store. It then consumes that local copy with a serial for loop.


Mel Structure Buffers

The above Producer and Consumer process one structured mel and then close. Let's modify them so that they run continuously via an infinite while loop:
#define NUMITEMS 3
struct s { int contents[NUMITEMS]; };
main () {
    <mel buffer> struct s store;
    <!>Producer (store);
    <!>Consumer (store);
}
/* VERSION 2 */
void Producer (<mel> struct s store) {
   int c;
   struct s localstore;
   while ( 1 ) {
      for (int i=0; i<NUMITEMS; ++i) {
          c = produce();
          if ( c == 0 ) {
             <close>store;
             return;
          }
          localstore.contents[i] = c;
      }  
      <?>store = localstore;
   }
}
/* VERSION 2 */
void Consumer (<mel> struct s store) {
   struct s localstore;
   while ( 1 ) {
      try {
         localstore = <?>store;
         for (int i=0; i<NUMITEMS; ++i) {
            consume (localstore.contents[i];
         }
      }
      catch ( store<CLOSED> ) {
        printf ("Exit because the Store is closed");
        return;
      }
    }
}
We also add a buffer attribute to the structured mel declaration. The goal is to optimize the throughput of the program. If the Producer is faster than the Consumer, it can deposit a finished structured mel product into the writer's side of the buffer while the Consumer can take its time removing deposited structured mel values from the reader's side.

A buffer attribute without any argument means that the above example uses the default number of buffer slots as provisioned by the CHAOS run-time environment. We can specify a different number of slots in the buffer:
/* Increase the default buffer size by 5 more from default */
<mel buffer+5> struct s store;
/* Or specify a fixed number for buffer storage */
<mel buffer=25> struct s store;
Each slot in a structured mel buffer must contain the full structured mel. Naturally, to buffer or not to buffer a structured mel, and to pick the proper buffer size, depends on the need of the NERWous C program: a buffer can increase the performance of the program at the expense of increased resource cost.


Mel Structures With Multiple Items

A C structure can contain heterogeneous items. Likewise, a NERWous C structured mel can contain heterogeneous items, and these items go together from a concurrent access point of view.

Let's now enhance the previous example to introduce a numrounds item to the mel struct stores. Every time the Producer finishes one round of updating contents, it will increase numrounds by one. The Consumer displays the round number before consume'ing the contents:
#define NUMITEMS 3
struct s { 
   int contents[NUMITEMS]; 
   long numrounds;
};
main () {
   <mel buffer> struct s store;
   <!>Producer (store);
   <!>Consumer (store);
}
/* VERSION 3 */
void Producer (<mel> struct s store) {
   int c;
   struct s localstore;
   localstore.numrounds = 0;
   while ( 1 ) {
      for (int i=0; i<NUMITEMS; ++i) {
          c = produce();
          if ( c == 0 ) {
             <close>store;
             return;
          }
          localstore.contents[i] = c;
      }
      localstore.numrounds++;   
      <?>store = localstore;
   }
} 
/* VERSION 3 */
void Consumer (<mel> struct s store) {
    struct s localstore;
    while ( 1 ) {
       try {
          localstore = <?>store;
          for (int i=0; i<NUMITEMS; ++i) {
            consume (localstore.contents[i];
          }
          printf ("Consumed from round [%ld]", localstore.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
For both the Producer and Consumer, the numrounds value is accessed locally, either before the localstore is uploaded to the structured mel store, or after the structured mel is downloaded back to localstore.


Mel Structure Value Property

In the Consumer example above, we use a local variable, localstore, to capture the read value of the structured mel store. As we know about mel result properties, when a task does a read to a mel (be it a simple, array or structured entity as in this chapter), the read value of the mel is cached locally in the value property.

Let's replace localstore with the value property:
/* VERSION 4 */
void Consumer (<mel> struct s store) {
    while ( 1 ) {
       try {
          <?>store;
          for (int i=0; i<NUMITEMS; ++i) {
            consume (store<value>.contents[i]);
          }
          printf ("Consumed from round [%ld]", store<value>.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
Once the value is cached after the mel read, <?>store;, that value can be accessed via the value property, store<value>, like any other local variable. Since this is a structured mel, we use the dot (.) notation to get to the inner items (contents and numrounds).


Mel Structure Direct Itemized Access

The title of this section is a mouthful. What it means is that can we access an item of a mel structure directly over the NERW network, instead of downloading the whole mel structure and accessing that item locally, like we did using the localstore variable or the value property? The answer is no, but it may look like we can.

Let's re-write the Consumer task:
/* VERSION 5 */
void Consumer (<mel> struct s store) {
    while ( 1 ) {
       try {
           for (int i=0; i<NUMITEMS; ++i) {
             consume (<?>store.contents[i]);
          }
          printf ("Consumed from round [%ld]", <?>store.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
The above snippet is syntactically correct. However, under NERWOUS, the statement:
consume (<?>store.contents[i])
means:
<?>store; consume (store<value>.contents[i]);
In other words, the full structured mel is downloaded behind-the-scene, and then the requested element is presented from the local entity. For the for loop above, each iteration means a repeat of the wait on the mel structure and a full download, instead of a single wait and download for all iterations as in previous versions. Thus even though VERSION 5 code looks more compact, its run-time performance is much worse.

In addition, the integrity of the data consume'd is doubtful. If the Producer has generated another round of products between the Consumer for loop iterations, contents[i] and contents[i+1] may come from different rounds of the mel structure store.

Structured mel direct itemized access does have its use as a compact way to access a single item of a mel structure when we don't need to access other items. This method is bad here for our Consumer task because we also need to access other items of the mel structure. For this, the download-and-local-access method is the better approach.


Mel Structures In Exclusive Zones

Like any mel, structured mels can be accessed via mel zones: reader zones and writer zones.

Mel Structures In Reader Zones

Once a task is in a reader zone for a structured mel, it blocks all read access to that mel. If the mel is hosted remotely, it copies the contents of the structured mel into a local eponymous structured variable. It then works with this local variable. On the checkout off the reader zone, it can either removes the structured mel it is holding (causing the next reader task to pick up the next structured mel value), updates the remote structured mel without removing it (allowing the next reader to pick the same structured mel but with the updated value), or skips doing neither a removal nor update (letting the next reader to pick the same structured mel with the existing value).

In all cases, the task updates the local sequence property of the structured mel to be the same as the sequence number of the structured mel on entry of the reader zone. In the update and skip cases, this update prevents the task from processing a stale structured mel again. Upon encountering a stale mel, the task just waits for a new structured mel value to be deposited by a writer task.

We will now modify our Producer/Consumer example, so that the Consumer task will make use of the reader zone. In fact, we will have two Consumer tasks. The DO_EVEN one only processes the structured mel if the product's round count (numrounds) is even. Likewise the DO_ODD one processes only on odd numrounds:
#define NUMITEMS 3
#define DO_EVEN 0
#define DO_ODD 1
struct s { 
   int contents[NUMITEMS]; 
   long numrounds;
};
main () {
   <mel buffer> struct s store;
   <!>Producer (store);
   <!>Consumer (store, DO_EVEN);
   <!>Consumer (store, DO_ODD);
}
/* VERSION 3 */
void Producer (<mel> struct s store) {
   int c;
   struct s localstore;
   localstore.numrounds = 0;
   while ( 1 ) {
      for (int i=0; i<NUMITEMS; ++i) {
          c = produce();
          if ( c == 0 ) {
             <close>store;
             return;
          }
          localstore.contents[i] = c;
      }
      localstore.numrounds++;  
      <?>store = localstore;
   }
} 
/* VERSION 6 */
void Consumer (<mel> struct s store, int evenodd ) {
    while ( 1 ) {
       try {
          <?>store {
             if (  ((evenodd == DO_EVEN) && (store.numrounds % 2) == 0))
                || ((evenodd == DO_ODD) && (store.numrounds % 2) != 0)) ) {

                for (int i=0; i<NUMITEMS; ++i) {
                   consume (store.contents[i]);
         
                printf ("Consumed from round [%ld]", store.numrounds);
                <checkout>;   /* optional here */
             }
             else {
                printf ("Skipped round [%ld]", store.numrounds);
                <checkout skip>;    /* logically required */
             }
          }

          showme (store<value>.contents);
      }
      catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}

void showme (int contents[]) {
    printf ("Contents array:\n");
    for (int i=0; i<NUMITEMS; ++i) printf ("%d - ", contents[i]);
}
Within the exclusive zone, the task makes use the eponymous local variable, store, and thus does not need the wait operator ? as when it accesses the remote mel variable store.

When the DO_EVEN Consumer task sees an even numrounds, it will do the consumption of contents and invokes the <checktout> statement to get out of the exclusive zone. Although that statement is not required (since this is the default behavior when the code falls off the exclusive zone code block), it is added explicitly here to balance with the use of <checkout skip> in the else code block. If the numrounds is odd, the DO_EVEN Consumer task does not want to consume the contents; instead it invokes <checkout skip> to get out of the exclusive zone without reading out the structured mel. Then the DO_ODD Consumer enters the reader zone, it will pick up this value and consumes it.

The showme function is introduced as an artifact to discuss the value property of a mel variable on checkout of a reader zone. In both the <checkout> and <checkout skip> cases, the value property reflects the value of the eponymous local variable at the time of the reader zone checkout. For example, for the DO_EVEN task, if the numrounds is even, showme will show the contents that are copied from the structured mel to the eponymous structured variable, consume'd, and then copied from the eponymous structured variable to the value property; if the numrounds is odd, showme will show the contents that are are copied from the structured mel to the eponymous structured variable, and then copied from the eponymous structured variable to the value property, without being consume'd.

Mel Structures In Writer Zones

To illustrate the use of structured mels with a writer zone, we now change the Producer task:
/* VERSION 4 */
void Producer (<mel> struct s store) {
   int counter = 0;
   while ( 1 ) {
      <?>store = {
          for (int i=0; i<NUMITEMS; ++i) {
              store.contents[i] = produce();
              if ( store.contents[i] == 0 ) 
                  <checkout skip>;
          }
          store.numrounds = ++counter;
      } /* implicit <checkout> */
   }
} 
The new Producer waits until it is at the top of the mel store writer queue and an empty slot opens up. The Producer then puts a hold on that slot, and enters the writer zone. Inside the zone, the local eponymous variable store is declared by default and available for Producer to use. It then starts to fill the store.contents with produce results.

If a result of a produce is 0, the Producer gets out of the writer zone with a <checkout skip> statement: it releases the hold of the empty slot of the mel buffer without writing anything to it. The next writer task can make use of that slot. In our example here, there is no other writer task besides Producer. Since Producer runs an infinite while loop, it will iterate and gets into the writer zone again. Hopefully this time the issue that causes produce to return 0 has been resolved, and Producer can fill all the NUMITEMS of the local store.contents. It then updates the local store.numrounds, then gets out of the writer zone by doing an implicit <checkout> statement at the closing } of the zone code block.

When a task invokes an explicit or implicit <checkout> operation, the eponymous variable that the task has been building inside the writer zone, is written into the empty slot on hold. The task then frees the hold to allow a reader task to access the new structured mel value.

The big difference between VERSION 3 and VERSION 4 of Producer is the holding of the empty slot to receive the write. In VERSION 3, the Producer builds the items of the structured mel store locally. Once everything is all built, it waits for the empty slot from the writer side of the mel buffer, and gets hold of it long enough to copy the items over. The VERSION 4 of Producer does the reverse. It makes sure that it has an empty slot to hold on to before it starts to do the produce'ing and updating the numrounds value. The latter method keeps the hold on the empty slot much longer, potentially making other writers to wait on accessing the mel. On the other hand, if what it produces is precious, it is willing to trade concurrency performance for the assurance that it can deposit its product.


Mel Structures Inside Local Structures

In the previous sections, we include new items into a mel structure to make it a multi-item mel structure. We will now do the reverse: we will include a mel entity inside a local structure. Inside this local structured, whatever declared local can only be accessed by the declaring task, and whatever declared as a mel is accessible by other tasks.

In the following example, we define struct sn to include a "serial number" identification that is local to Producer which uses it to stamp an identification into each of its products. The produced items are still stored in the mel structure store which is shared between the Producer and the Consumer. However the Consumer does not have access to the "serial number" information used solely by the Producer.
#define NUMITEMS 3
struct s { 
     int contents[NUMITEMS]; 
     long numrounds;
};
struct sn {
 int snid;
 <mel> struct s store;
}
main () {
     struct sn localsn;
     localsn.snid = 8475;     /* starting serial number */
     localsn.<?>store = {1, 2, 3}, 0L); /* seed the store */

     <!>Producer (localsn);
     <!>Consumer (localsn.store);
}
/* VERSION 5 */
void Producer (struct sn localsn) {
   try {
      struct s localstore;
      localstore.numrounds = 0;
      while (1) {
         for (int i=0; i<NUMITEMS; ++i) {
            int c = produce(localsn.snid++);
            if ( c == 0 ) {
                <close>localsn.store;
                break;
             }
             localstore.contents[i] = c;
         }
         localstore.numrounds++;
         localsn.<?>store = localstore;
      }
   }
   catch ( localsn.store<NOREADERS> ) {
        printf ("Exit because no more Consumer");
        return;
   }
} 
/* VERSION 4 */
void Consumer (<mel> struct s store) {
    while ( 1 ) {
       try {
          <?>store;
          for (int i=0; i<NUMITEMS; ++i) {
            consume (store<value>.contents[i]);
          }
          printf ("Consumed from round [%ld]", store<value>.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
We re-use VERSION 4 of the Consumer code. For the Producer, its argument is changed to the local struct localsn which contains the structured mel store. Producer accesses store via the construct:
localsn.<?>store
if outside a mel exclusive zone (like in Producer above), and via:
localsn.store
if inside an exclusive zone (as in the Consumer below).

What if we also pass localsn to Consumer? Let's look at this code:
/* VERSION 7 */
void Consumer (struct sn localsn) {
    printf ("First serial number [%d]", localsn.snid);
    try localsn.<?>store {
     printf ("Updated serial number [%d]", localsn.snid);
     printf ("Consuming from round [%ld]", localsn.store.numrounds);
        for (int i=0; i<NUMITEMS; ++i) {
            if (! consume (localsn.store.contents[i]) ) 
                return;      /* triggering the NOREADERS exception at Producer */
         }
         <resume>;     /* back to the mel zone wait for more consumption */
    }
    catch ( localsn.store<CLOSED> ) {
        printf ("Exit because the Store is closed");
        return;
    }
    catch ( localsn.store<NOWRITERS> ) {
        printf ("Exit because there is no more Producer");
        return;
    }
}
The "First" printf statement will show the value 8475 initialized by the task main at the time that it creates the task Consumer via the pel <!> statement. In each round going through its read zone, Consumer also does an "Updated" printf statement. This statement still shows the value for localsn.snid to be 8475, not whatever value Producer has at that moment. This is because locansn.snid is local to each task, and the above Consumer has not changed the value of this variable since accepting it from main. On the other hand, the localsn.store.contents and localsn.store.numrounds will reflect the new values from Producer since they are items of a structured mel.


Previous Next Top

Thursday, August 18, 2016

Mel Arrays

Welcome » NERWous C » Mel
  1. Mel Arrays
  2. Mel Array Value Property
  3. Mel Array Closures
  4. Mel Array Status Property
  5. Mel Array Exceptions
  6. Mel Array Boundary Exceptions
  7. Mel Array Count Property
  8. Mel Array Boundary Behaviors
  9. Mel Array Consecutive Constructs
  10. Mel Array OR Wait Examples
  11. Mel Array AND Wait Examples
  12. Mel Array LIST Wait Examples


Mel Arrays

So far in our previous examples, the mel variable is a single entity (for simplicity, we have chosen it to be an integer). Now let's explore how to handle a collection of mel entities of the same type using the mel array construct. In the new example below, we will have the Producer produces a series of products to be consumed:
#define NUMITEMS 30
#define NUMROUNDS 5
main () {
     <mel> int stores[NUMITEMS];
     <!>Producer (stores);
     <!>Consumer (stores);
}
/* PRODUCER VERSION 1 */
void Producer (<mel> int stores[]) {
     int round = 0;
     while (1) {
         for (int i=0; i<NUMITEMS; ++i) 
         <!collect> {
             <?>stores[i] = produce();
         } <? ENDED>;
         printf ("Done with round [%d]\n", round);
         if (++round >= NUMROUNDS) break;
     }
     <close>stores;
}
The main program forks the task Producer to run in parallel with the task Consumer. Within Producer, the tasks to produce are also run in parallel via the for-pel loop. Unlike a serial for loop, it is possible with the for-pel loop to have a stores[i+1] produce'd before a stores[i] since the produce'ing tasks may be assigned to different cels with different performance and load.
The value i is passed from the Producer task to each produce inline task via the automatic local variable import facility.
The tasks Producer and Consumer share the access to the mel array stores. Each element within a mel array is an independent mel entity. They are produced and consumed separately from one another. For example, the for-pel task for stores[i] may be suspended to wait for a consumer to retrieve that value and free the mel array item for a new write, while the for-pel task for stores[i+1], working with a faster consumer, has no problem depositing its product to its mel array item.

By using the collect-ENDED construct, the task Producer waits for all the inline tasks it pels to be finished. This marks the end of the first round. Producer then starts the second round with a new set of for-pel tasks. After NUMROUNDS rounds, the Producer task is done. Before ending itself, it calls the close operation on the mel array stores to inform any Consumer tasks of no more forthcoming products.


Mel Array Value Property

Let's now use the Consumer task to explore the value property for a mel array.
/* CONSUMER VERSION 1 */
void Consumer (<mel> int stores[]) {
   for (int i=0; i<NUMITEMS; ++i)
   <!> {
      while (1) {
         consume (<?>stores[i]);
         printf ("Just consumed item[%d] of value [%d]\n", i, stores[i]<value>);
      }
   }
}
Unlike the Producer which creates a new set of NUMITEMS tasks at each NUMROUND iteration, the Consumer uses a serial for loop to create a fixed set of tasks, one for each store element. Each task runs an infinite while loop to keep consume'ing its assigned store item, no matter in what round in Producer that that item has been produce'd.

Each while-loop inline task invokes a read operation with <?>stores[i]. Upon success, the read operation returns the read value as the result of the operation. This result is then consumed by the consume function. It also saves the result in the value property of the local cache representing the mel variable store[i]. This value property is later used as the argument for the printf statement.

Since there is no exception handler, all while loop inline tasks will automatically end via abortion when the mel array is closed by the Producer. With all tasks ended, the program can exit.

The while loop can be replaced by the resume operation:
/* CONSUMER VERSION 2 */
void Consumer (<mel> int stores[]) {
   for (int i=0; i<NUMITEMS; ++i)
   <!> {
      consume (<?>stores[i]);
      printf ("Just consumed item[%d] of value [%d]\n", i, stores[i]<value>);
      <resume>;
   }
}
The resume operation jumps back to the latest wait statement which is <?>stores[i], which is what the while(1) loop was doing


Mel Array Closures

Let's revisit the Producer use of
<close>stores;
That statement is semantically equivalent to closing all the independent mel elements in the mel array in parallel:
for (int i=0; i<NUMITEMS; ++i) <!> { <close>stores[i]; }
It is also possible to close a particular element separately:
/* PRODUCER VERSION 2 */
void Producer (<mel> int stores[]) {
   int round = 0;
   while (1) {
      for (int i=0; i<NUMITEMS; ++i) 
      <!collect> {
         int c = produce();
         if ( c == 0 ) <close>stores[i];
         else <?>stores[i] = c;
      } <? ENDED>;
      if (++round >= NUMROUNDS) break;
   }
   <close>stores;
}
Here, Producer can prematurely close an element in the mel array if the "product 0" condition is true for that element. After NUMROUNDS rounds, the Producer task closes the rest of the elements in the mel array. The <close>stores statement skips any elements that have already closed.


Mel Array Status Property

There is an issue with the Producer code above. It runs the for-pel loop NUMROUND times. What if it closes an element of the mel array in an earlier round and then attempts to produce for that item in the subsequent rounds? Since that element of the mel array has been closed, any subsequent write access to it will fail.

Let's modify the Producer to tackle this concern:
/* PRODUCER VERSION 3 */
void Producer (<mel> int stores[]) {
   int round = 0;
   for (int i=0; i<NUMITEMS; ++i) 
   <!collect> {
      while ( ++round < NUMROUNDS ) {
         if ( stores[i]<status> == NERW_STATUS_CLOSED ) <end>;
         int c = produce();
         if ( c == 0 ) <close>stores[i];
         else <?>stores[i] = c;
 
         if (++round >= NUMROUNDS) break;
      }
   } <? ENDED>;
   <close>stores;
}
The VERSION 3 of Producer flips the order of the for and while loops from the VERSION 2. In VERSION 2, we have a while loop that runs NUMROUNDS times, and each time, it pels NUMITEMS inline tasks and waits for them to finish. The total number of tasks that are created is therefore NUMROUNDS * NUMITEMS. In the new VERSION 3, we have a for loop that pels NUMITEMS tasks. Each task then runs its own while loop sequentially for NUMROUNDS times. The number of inline tasks created in VERSION 3 is much smaller than in VERSION 2.

When an inline task produces a 0 value, it closes its mel array element <close>stores[i]. We could end that inline task here. However for the sake of discussion, we allow it to loop back and does another round. The first thing it does is to check for the mel status property since there is no point of produce'ing if there is no stores element to put it into. The status propery value has been piggy-backed in the acknowledgement return package of the last operation to the mel entity, and saved in the local property cache. If the mel entity has been close'd from the previous round, its status will be NERW_STATUS_CLOSED on this round.

What would happen if the inline task did not check for the status of the mel array element that it plans to use? It would run produce. Then when it were to deposit that product to the mel array element with the mel write statement, <?>stores[i] = c, it would get a stores[i]<CLOSED> exception. Since it did not have a handler for this exception, it would also end, but now via a pel abortion.


Mel Array Exceptions

Let's now re-write the Consumer task to go with the above Producer, highlighting the use of exceptions with mel arrays:
/* CONSUMER VERSION 3 */
void Consumer (<mel> int stores[]) {
   int numitems = stores<count>;
   for (int i=0; i<numitems; ++i) {
      printf ("Task for [" + i + "] created");
      <!> while (1) { 
         try { consume(<? timeout>stores[i]); }
         catch ( stores[i]<TIMEDOUT> ) { 
            printf ("Keep trying");
            <resume>; 
         }
         catch ( stores[i]<CLOSED> ) { 
            printf ("stores[%d] has closed", i); 
            break; 
         }
         catch ( stores<CLOSED> ) { 
            printf ("the full array stores has closed"); 
            break; 
         }
         catch ( stores[i]<BOUNDARY> ) { 
            printf ("Boundary error on [%d] - Abort! ", i);
            break; 
         }
      }
   }
}
The above Consumer uses a serial for loop to print the "Task for ..." message and then pel a number of inline tasks, one for each item of the stores array. Once Consumer has done all the task creations, it will end, leaving its inline tasks to end on their own terms via one of the exception handlers. Since Consumer does not care when its inline tasks end, it does not have to wrap the for loop in a collect-ENDED construct like the Producer task.

Each inline task does an infinite while loop. For each iteration of the loop, the task waits for its own stores[i] mel element. The wait is limited by a default timeout. When the mel is valued, it is retrieved and consume'd. Then the wait resumes.

The TIMEOUT exception and the first CLOSED exception handlers are applicable to the specific stores[i] mel element. The second CLOSED exception handler, stores<CLOSED>, applies to the whole mel array.

There are three ways for an inline task to end. It can break out of the while loop when its mel element is closed, or when the whole mel array is closed. After getting off the while loop, with nothing else to do, the task ends.

The third way is via a boundary exception which is the topic of the next section.


Mel Array Boundary Exception

There is a new exception associated with a mel array, the boundary exception, as seen in the VERSION 3 of Consumer:
catch ( stores[i]<BOUNDARY> ) { 
    printf ("Boundary error on [%d] - Abort! ", i);
    break; 
}
This exception is raised if the mel element being accessed is beyond the mel array contents. For example, the mel array stores with NUMITEMS items contains stores[0] to stores[NUMITEMS-1]; accessing stores[-1] or lower will trigger this out-of-bound exception at the lower end, and accessing stores[NUMITEMS] or above will trigger this exception at the upper end.

Although for the Consumer version above, the for (int i=0; i<numitems; ++i) does not go out-of-bounds, and thus Consumer will not be hit by the Boundary exception, it is still good to include this exception handler for defensive programming.


Mel Array Count Property

The boundary exception depends on CHAOS knowing the items count in the mel array. Whenever a mel array argument is passed to a pel task, the items count is also passed along and cached locally as the count property of the mel array. To properly iterate through the mel array, the task Consumer makes use of the count property, instead of using the constant NUMITEMS as in the Producer:
int numitems = stores<count>;
NERWous C library functions that pushes items in or pops items out of a mel array are required to update the count value at the mel array itself. When such an action occurs, the count properties locally cached in the tasks will become stale. These tasks need to refresh their property cache either by accessing the mel array again for reads or writes, or by using the snaphot operation.


Mel Array Boundary Behaviors

The Consumer VERSION 3 handles the mel array boundary in two ways. We make sure that stores[i] does not go out of bounds by controlling the value of i. We also handle any out-of-bound condition via the boundary exception handler. Boundary behaviors is the third way for handling boundary conditions.

A boundary behavior sets up an automatic default handling when stores[i] is out-of-bound so that a boundary exception can be either enforced or avoided. NERWous C supports the following behaviors:
  • Strict behavior:
    NERWous C strictly enforces the boundaries. If the item of the mel array is out of bound, the boundary exception is raised.

    This is the default behavior if no behavior is specified, unless the out-of-bound condition happens in a catch construct (see the flex behavior below).
     
  • Flex behavior:
    NERWous C tries to accommodate the out-of-bound conditions as much as possible to prevent the boundary exception. This is accomplished via two methods:
    • Ignoring out-of-bound items if there are other items to process. For example, out-of-bound items in an OR list wait can be ignored, and the result of the OR list wait depends on the in-bound items. This method is the default for both reading and writing.
    • Stand-in values: this method is for reading only. When an item is out-of-bound, its value will be the stand-in value instead of causing an exception.
    If there are no stand-in values and all the items are out-of-bound, the flex behavior cannot prevent the boundary exception from occurring.

    The flex behavior is the default behavior in a catch construct as seen later in the Mel Array With Writer OR Zones example.
     
  • Wrap and Wrapflex behaviors:
    NERWous C wraps around the out-of-bound items. Out-of-bound items in the upper boundary will wrap around to the lower boundary, and vice versa. Boundary conditions can still occur if there are not enough items in the mel array to accommodate the wrap around. In this case, the wrap behavior falls back to the strict behavior, and the wrapflex behavior, to the flex behavior.

    The wrapped-around item uses the same mel access as the out-of-bound item. For example, if the latter uses a mel readonly access, the former also uses readonly access.

These are the attributes of the boundary behaviors:
lboundThe specified behavior will be triggered on any array item that has its index equal to or lower than this lower boundary index. The default value is -1.
lvalThe flex and wrapflex behaviors use this lower boundary value as the stand-in value for any array item at or below the lower boundary. If not specified, the default value is 0 for numerical items and null for non-numerical items.
uboundThe specified behavior will be triggered on any array item that has its index equal to or greater than this upper boundary index. The default value is the array count property.
uvalThe flex and wrapflex behaviors use this upper boundary value as the stand-in value for any array item at or above the upper boundary. If not specified,the default value is 0 for numerical items and null for non-numerical items.

This is the coding format for the boundary behaviors, using stores[i]:
mel stores[20];
/* strict behavior - explicit */
<?>stores[i]<strict lbound=5 ubound=10>;

/* flex behavior */
<?>stores[i]<flex lbound=5 ubound=10 lval="111" uval="999">;

/* wrap behavior */
<?>stores[i]<wrap lbound=5 ubound=10>;

/* wrapflex behavior */
<?>stores[i]<wrapflex lbound=5 ubound=10 lval="111" uval="999">;
Although the mel array stores can contain 20 items, the above statements work on a subset of the array. With lbound set to 5, stores[0] to stores[5] will trigger the stated boundary behavior. Likewise, with ubound set to 10, stores[10] to stores[19] will trigger the stated boundary behaviors. Either or both of the attributes can be omitted, and the boundary behaviors use the default value of -1 for the lower bound, and 20 for the upper bound, which is the full size of the mel array.

The lval and uval attributes contain the stand-in values, and are needed only for the flex and wrapflex behaviors.


Mel Array Consecutive Constructs

When consecutive mel array items are used in an OR, AND or LIST reads or writes, instead of listing them individually, we can use the consecutive 2-dot construct (..), and specify only the first and last item.

The boundary behaviors can also be used with consecutive constructs. When specified, a behavior is applicable to all the mel array items in the consecutive construct. If different behaviors happen to be needed, the consecutive construct should not be used. Instead, the explicit list of the items will allow different behaviors to be assigned to different items.

The following code snippets use stores[i] Consecutive OR:
Explicit<?>(stores[i] || stores[i+1] || stores[i+2])
Consecutive<?>(stores[i] ..||.. stores[i+2])
with Strict<?>(stores[i] ..||<strict lbound=5 ubound=10>.. stores[i+2])
with Flex<?>(stores[i] ..||<flex lbound=5 ubound=10 lval="111" uval="999">.. stores[i+2])
with Wrap<?>(stores[i] ..||<wrap lbound=5 ubound=10>.. stores[i+2])
with Wrapflex<?>(stores[i] ..||<wrapflex lbound=5 ubound=10 lval="111" uval="999">.. stores[i+2])

Consecutive AND:
Explicit<?>(stores[i] && stores[i+1] && stores[i+2])
Consecutive<?>(stores[i] ..&&.. stores[i+2])
with Strict<?>(stores[i] ..&&<strict lbound=5 ubound=10>.. stores[i+2])
with Flex<?>(stores[i] ..&&<flex lbound=5 ubound=10 lval="111" uval="999">.. stores[i+2])
with Wrap<?>(stores[i] ..&&<wrap lbound=5 ubound=10>.. stores[i+2])
with Wrapflex<?>(stores[i] ..&&<wrapflex lbound=5 ubound=10 lval="111" uval="999">.. stores[i+2])

Consecutive LIST:
Explicit<?>(stores[i], stores[i+1], stores[i+2])
Consecutive<?>(stores[i] ... stores[i+2])
with Strict<?>(stores[i] ..<strict lbound=5 ubound=10>.. stores[i+2])
with Flex<?>(stores[i] ..<flex lbound=5 ubound=10 lval="111" uval="999">.. stores[i+2])
with Wrap<?>(stores[i] ..<wrap lbound=5 ubound=10>.. stores[i+2])
with Wrapflex<?>(stores[i] ..<wrapflex lbound=5 ubound=10 lval="111" uval="999">.. stores[i+2])

For a consecutive LIST, the unwieldy ..,.. construct is shortened to an ellipsis, .... On the same vein, the comma is also omitted in consecutive LIST constructs with boundary behaviors.


Mel Array OR Wait Examples

The following examples show the use of mel arrays with OR waits:
  1. Mel Array with OR Writes
  2. Mel Array with OR Reads
  3. Mel Array with Reader OR Zones
  4. Mel Array with Writer OR Zones

Mel Array With OR Writes

Let's start with mel OR writes by modifying the Producer so that it can write to either stores[i] or a common overflowstore.
/* PRODUCER VERSION 4 */
void Producer (<mel> int stores[], <mel> int overflowstore) {
     int round = 0;
     for (int i=0; i<NUMITEMS; ++i) 
     <!collect> {
         while ( ++round < NUMROUNDS ) {
              int c = produce();
             if ( c == 0 ) { 
                 <close>stores[i];
                 break;
             }
             <?>(stores[i] || overflowstore) = c;
         }
         if (++round >= NUMROUNDS) break;
     } <? ENDED>;
     <close>stores;
}
This version of Producer adds to the VERSION 3 the overflow mel variable. If stores[i] is full and cannot accept a new value, the new Producer will try the overflow variable. Unlike stores[i] which is exclusive to a for pel task, the overflow mel variable is shared between all the for pel tasks.

Mel Array With OR Reads

To illustrate the mel OR reads, let's modify the Consumer to read from either stores[i] or if it is empty, from overflowstore.
/* CONSUMER VERSION 4 */
void Consumer (<mel> int stores[], <mel> int overflowstore) {
    int numitems = stores<count>;
    for (int i=0; i<numitems; ++i) {
        printf ("Task for [" + i + "] created");
        <!> while (1) { 
            try { 
                consume(<? timeout>(stores[i] || overflowstore)); 
            }
            catch ( stores[i] || overflowstore)<TIMEDOUT> ) { 
                printf ("Keep trying"); 
                <resume>; 
            }
            catch ( stores[i]<CLOSED> ) { 
                printf ("Only me out!"); 
                break; 
            }
            catch ( stores<CLOSED> ) { 
                printf ("Everybody out!"); 
                break; 
            }
            catch ( stores<BOUNDARY> ) { 
                printf ("Boundary error - Abort! ");
                break; 
            }
            catch ( overflowstore<CLOSED> ) { 
                printf ("Keep trying with stores[i] only");
                <resume>; 
            }
        }
     }
}
There is a flaw in the above Consumer. A value in overflowstore may be originally destined to any item in the mel array stores, and not necessarily to stores[i]. This is because the same overflowstore is used by all the Producer tasks to deposit their "overflown" product.

Mel Array With Reader OR Zones

The previous Consumer makes use of only one item in the stores array. Let's modify Consumer to use 3 consecutive items in the array, to illustrate the use of an reader OR zone in conjunction with a consecutive OR construct.
/* CONSUMER VERSION 5 */
void Consumer (<mel> int stores[], <mel> int overflowstore) {
  int numitems = stores<count>;
  for (int i=0; i<numitems; ++i) 
  <!> {
    try <? as=consumable>(stores[i] ..||<flex>.. stores[i+2] || overflowstore) {
      printf ("Item to be consumed [%s] of value [%d]\n", consumable<name>, consumable);
      consume (consumable);
    } <resume>;     // use resume to loop back instead of while(1) loop
    catch ( (stores[i] ..&&.. stores[i+2] && overflowstore)<CLOSED> ) { }
  }
}
With the flex boundary behavior, if stores[i+1] or stores[i+2] are out-of-bound, they will be ignored in the processing of the OR list.

The consumable is the eponymous variable. It can be in the NERWous C mel format as in consumable<name> or in the basic C data type format, consumable. In fact, when used inside an exclusive zone, consumable is a shortcut for consumable<value>.

Mel Array With Writer OR Zones

Let's now modify Producer task to use a writer OR zone with consecutive mel array items.
/* PRODUCER VERSION 5 */
void Producer (<mel> int stores[], <mel> int overflowstore ) {
  int round = 0;
  while (1) {
    for (int i=0; i<NUMITEMS; ++i) 
    <!collect> {
      try <?writer mode=random as=product> 
      (stores[i] ..||<wrap>.. stores[i+2] || overflowstore) {
        product = produce();
        if ( product == 0 ) { 
          <close>product;
          printf ("Item [%s] is closed\n", product<name>);
        }
        printf ("Item [%s] contains value [%d]\n", product<name>, product);
      }
      catch ( (stores[i] ..&&.. stores[i+2] && overflowstore)<CLOSED> ) { 
        printf ("All items in OR list have closed\n");
      }
    } <? ENDED>;    
    if (++round >= NUMROUNDS) break;
  }
  <close>stores;
  <close>overflowstore;
}
The previous Producer VERSION 4 has a while loop inside a for loop, and pels exactly NUMITEMS tasks. For the sake of difference, this VERSION 5 reverses the order, and has a for loop inside a while loop. It pels NUMITEMS tasks every NUMROUNDS iterations, for a total of NUMITEMS * NUMROUNDS tasks for the life of Producer.

The writer attribute in the mel wait statement indicates that this wait is for a writer zone. When a task is inside the writer zone, it will prevent other tasks to access the writer's end of the mel variable to deposit a new value.
Is using a writer zone for this version of Producer an overkill, since a simple OR write statement can suffice?
<?> (stores[i] ..||<wrap>.. stores[i+2] || overflowstore) = produce();
The main difference is that with the writer zone, the mel access for writing is held up during the time the product is being produce'd, while with the simple write statement, the hold-up is only to deposit a value that has been already produce'd.
The OR list on entrance of the writer zone is also used on the checkout of the zone. The eponymous variable product has the value to be written into the mel variable. But which mel variable in the OR list? Since the random mode is used, the CHAOS runtime will randomly select either stores[i], stores[i+1], stores[i+2], or overflowstore. If it the first randomly selected mel variable has an empty slot, it will be put on hold while the task is in the writer zone, and on checkout, it will receive the value of the eponymous variable. If that mel variable is still full, CHAOS will randomly look at another mel variable in the list. If none of the variables are available, CHAOS will force the task to wait at the mel wait statement <?> until one mel variable is available for writing.

To handle boundary conditions, the wrap behavior starts to replace stores[i+1] and stores[i+2] with stores[0] and stores[1] when i reaches the end of the stores mel array (which is NUMITEMS-1).

When a 0 value is produced, the Producer decides to close the mel variable that has been selected, and currently represented by the eponymous variable product. Once that mel variable is closed, on the next round of the while loop, CHAOS will skip looking at this mel variable during the processing of the OR list.

If it happens that all the mel variables in the OR list have been previously closed, the mel zone wait statement will abort with the closure exception. Here, the exception handler just displays the message and implicitly ends the task.

The closure exception uses the AND consecutive construct for the mel array items (stores[i] ..&&.. stores[i+2]). When used in the catch construct, the flex behavior is implicit. This prevents a boundary exception when we are already handling a boundary condition.

Talking about implicit, there is an implict checkout of the writer zone at the closing }. This brings up the default behavior which is to update the writer's end of the selected mel variable with the value of the eponymous variable product.

Each while loop iteration pels NUMITEMS tasks and uses the collect-ENDED construct to wait for all the pelled tasks to finish, before the next round is started. It is also possible to omit the collect-ENDED construct. In that case, from NUMITEMS to NUMITEMS * NUMROUNDS tasks can exist at the same time and run in parallel. They will vie for the same stores and overflowstore shared resources but the writer zone wait will ensure that only one task can check out a resource, and the checkout follows the chronological order of the pel creations.

When NUMROUNDS rounds have completed, the Producer tasks closes the mel array stores and the mel variable overflow. The corresponding Consumer task will cue on these closures to end itself.


Mel Array AND Wait Examples

Let's now modify Consumer and Producer to illustrate the use of the AND operation with mel arrays. We will skip the AND reads and AND writes discussion, and delve only with AND zones:
  1. Mel Array with Reader AND Zones
  2. Mel Array with Writer AND Zones

Mel Array with Reader AND Zones
Unlike previous versions, this new Consumer consumes, not one but multiple items from the mel array store on every run. These mel elements are waited on using the reader AND zone:
/* CONSUMER VERSION 6 */
void Consumer (<mel> int stores[], <mel> int overflowstore) {
    int numitems = stores<count>;
    for (int i=0; i<numitems; ++i) 
    <!> {
        try <?>(stores[i] ..&&<flex uval=0>.. stores[i+2] && overflowstore)  {
            consume (stores[i]); 
            consume (stores[i+1]); 
            consume (stores[i+2]);
            consume (overflowstore); 
        } <resume>;     // loop back to zone wait
        catch ( (stores[i] ..||.. stores[i+2] || overflowstore)<CLOSED> ) { 
            break; 
        }
     }
}
There may be long waits at the reader AND zone entrance because each task iteration competes not only for its stores element (stores[i]) but also of the next two elements (stores[i+1] and stores[i+2]). And everybody competes for the same overflowstore mel variable. Here overflowstore acts as the serializer that allows only one task to be in the reader AND zone at a time. The mel AND wait will ensure that there is no deadlocks nor resource starvation, but the mechanism of an AND wait can impose performance penalty.

The AND list above uses the flex behavior with stand-in value 0 for any upper out-of-bound mel array items. For example, the last task that the for loop pels has i equals to NUMITEMS-1. Thus, stores[i] is in-bound, but stores[i+1] and stores[i+2] are out-of-bound. Instead of causing a boundary exception, they are assigned the stand-in value of 0, which is then consume'd inside the reader zone.

Mel Array with Writer AND Zones
To illustrate the use of a writer AND zone, let's modify the previous version of Producer:
/* PRODUCER VERSION 6 */
void Producer (<mel> int stores[], <mel> int overflowstore ) {
    int round = 0;
    while (1) {
       for (int i=0; i<NUMITEMS; ++i) 
       <!> {
           try <?writer>( stores[i] ..&&<flex>.. stores[i+2] && overflowstore)  {           
               if ( ! (stores[i] = produce()) )
                   <close>stores[i];
               if ( i < NUMITEMS-1 ) {
                   if (! (stores[i+1] = produce()) )
                       <close>stores[i+1];
               }
               if ( i < NUMITEMS-2 ) {
                   if ( ! (stores[i+2] = produce()) )
                       <close>stores[i+2];
               }
               if ( ! (overflowstore = produce()) )
                   <close>overflowstore;
           }
           catch ( (stores[i] ..||.. stores[i+2] || overflowstore)<CLOSED> ) {
               printf ("A required item has been closed by another task -- Abort");
               <end>;
           }           
       }
       if (++round >= NUMROUNDS) break;
    }
    <close>stores;
    <close>overflowstore;
}
This Producer also uses the flex behavior for its AND list. Since this is for a writer zone, there is no stand-in upper-bound value. Instead, any mel array item that is out-of-bound will be ignored. On entrance of the writer zone, the AND wait will skip checking stores[i+1] and stores[i+2] when i == NUMITEMS-1. On the same vein, on checkout of the writer zone, stores[i+1] and stores[i+2] will not be updated by the checkout operation if they are out-of-bound.

Inside the writer zone, the eponymous variables that stand for stores[i+1] and stores[i+2] can still be assigned values. But as said above, during the checkout, these values will not be used if the mel array items are out-of-bound. To prevent un-necessary produce activities which may deplete production resources, there are checks to validate i before produce is invoked.

The first task that produces a 0 value for overflowstore will cause subsequent tasks to hit the CLOSED exception. This is due to all the tasks include overflowstore in their AND mel waiting list.


Mel Array LIST Wait Examples

As we have discussed OR and AND operations with mel arrays, we will now wrap up this chapter with a discussion on LIST operations. Again we will skip the LIST reads and LIST writes, and focus on LIST zones.
  1. Mel Array with Reader LIST Zones
  2. Mel Array with Writer LIST Zones

A LIST exclusive zone requests all the specified mel elements like the AND exclusive zone, but unlike the AND zone, it holds on to a mel element whenever it is available. This is efficient but can result in deadlocks or starvation if other tasks are also requesting the same elements.

Single mel elements required by a LIST zone are stringed together via the comma (either for reads or writes, while consecutive LIST elements in a mel array uses 3-point ellipsis construct (...).

Mel Array with Reader LIST Zones
Let's again modify the Consumer task, this time to use the mel array stores with a reader LIST zone:
/* CONSUMER VERSION 7 */
void Consumer (<mel> int stores[], <mel> int overflowstore) {
    int numitems = stores<count>;
    for (int i=0; i<numitems; ++i) 
    <!> {
        try <?>(stores[i] ..<flex uval=0>.. stores[i+2],  overflowstore)  {
            consume (stores[i]); 
            consume (stores[i+1]); 
            consume (stores[i+2]);
            consume (overflowstore); 
        } <resume>;     // loop back to zone wait
        catch ( (stores[i] ..||.. stores[i+2] || overflowstore)<CLOSED> ) { 
            break; 
        }
     }
}
The difference between this LIST version and the "AND" version is the replacement of the consecutivve AND operator (..&&.. with the consecutive LIST operator (...). The "LIST" version also waits for all the specified elements -- stores[i], stores[i+1], stores[i+2], and overflowstore. However, while an AND wait requires all the elements to be available at the same time to prevent deadlock, the LIST wait allows the task to hold on a mel variable as it is made available. The task enters the reader LIST zone when it has accumulated holds on all the specified mel variables.

Mel Array with Writer LIST Zones
We now replace the writer AND zone of the previous Producer with a writer LIST zone to illustrate the use of such zone with mel arrays:
/* PRODUCER VERSION 7 */
void Producer (<mel> int stores[], <mel> int overflowstore ) {
    int round = 0;
    while (1) {
       for (int i=0; i<NUMITEMS; ++i) 
       <!> {
           try <?writer>( stores[i] ..<flex>.. stores[i+2], overflowstore)  {           
               if ( ! (stores[i] = produce()) )
                   <close>stores[i];
               if ( i < NUMITEMS-1 ) {
                   if (! (stores[i+1] = produce()) )
                       <close>stores[i+1];
               }
               if ( i < NUMITEMS-2 ) {
                   if ( ! (stores[i+2] = produce()) )
                       <close>stores[i+2];
               }
               if ( ! (overflowstore = produce()) )
                   <close>overflowstore;
           }
           catch ( (stores[i] ..||.. stores[i+2] || overflowstore)<CLOSED> ) {
               printf ("A required item has been closed by another task -- Abort");
               <end>;
           }           
       }
       if (++round >= NUMROUNDS) break;
    }
    <close>stores;
    <close>overflowstore;
}
Near the boundary condition, when i == NUMITEMS-1, the flex behavior instructs the LIST wait to ignore the out-of-the-bound mel array item, stores[i+2], and wait only on the inbound mel array items, stores[i] and stores[i+1], plus the mel variable overflowstore. Unlike an AND wait which waits for all these mel variables to be all available at the same time, the LIST zone wait allows the Producer to accumulate the holds of these mel variables as they become available. This wait is more efficient but may introduce deadlocks if another task is doing the same wait.


Previous Next Top