Pages

Sunday, December 2, 2018

Mel Operations

Welcome » NERWous C » Mel
  1. Mel Operations List
  2. Mel Creation Operation
  3. Mel Passing Operation
    1. Passing By Value
    2. Short Passing By Value
    3. Passing By Reference
    4. No Pointers
  4. Mel Operations Error Codes
    1. Errors In Creation
    2. Errors In Reads
    3. Errors In Writes
    4. No Errors
  5. Mel Operations Error Descriptions
  6. Mel Operations Exceptions
    1. Catch-All Exception
    2. Exception Handler Matching
    3. Exception Escalation
    4. Exceptions Clearing
    5. Exceptions vs. Errors
  7. Mel Snapshot Operation
    1. Snapshot Updated Properties
    2. Snapshot Monitoring Example
    3. Snapshot Operation Attributes


Mel Operations List

An operation is a request that a task sends to the CHAOS runtime to do something with a mel element. Mel operations are specified in lower case within the < > markers, and placed before a mel variable. (If placed after a mel variable, it would be a property.) In the following table, the operations are for the mel element represented by the mel variable store. The abbreviation R/W stands for Read/Write:

OperationSynopsis
<close>storeDeallocate the mel element store
<extern>storeRe-create the mel global variable store for a task
<quit>storeEnd the R/W subscriptions to the mel element store
<rebuffer>storeResize the R/W buffer of the mel element store
<resume>Resume R/W waiting on the mel element that causes an exception
<snapshot>storeSnapshot the mel element to update the mel variable

There are two Read / Write mel operations. They make use of the mel wait operator <?>:

OperationSynopsis
<?>storeWait for the mel element store to be available and filled, and retrieve the value of the top slot from the reader's side of the mel buffer.
<?>store =Wait for the mel element store to be available and vacant, and deposit a new value to the next slot from the writer's side of the mel buffer.

Another set of mel operations do not follow the < > markers format above in order to blend with existing C language constructs:

OperationSynopsis
Mel CreationAllocate a mel element and set up a resident mel variable
Mel Argument PassingPass a mel variable by value from one task to another
Mel Global VariablePass a global mel variable from one task to another



Mel Creation Operation

In order to do something with a remote mel element, a task has to first create a resident mel variable to represent the communication channel between itself and the mel element:
<mel> int store1;
<mel buffer=2 at=ssd_drive mode=readonly"> int store2;
The first statement creates a store1 mel variable with default attributes. The second statement creates store2 with explicit attributes:

OperationAttributesSynopsis
<mel> int store Mel creation operation
buffer
=n
Create a buffered mel element with n slots
at
=cel
Create the mel element at the specific cel location
mode
=setting
Create the mel element with this mode setting:
reader: this is the default mode.
readonly: the mel access is always read-only

A mel creation statement is both a compile-time declaration and a run-time operation. Being the latter, it can fail in real time. There are several ways to capture potential mel creation failures:
  1. Check the mel <error> property for non-0 value.
     
  2. Capture a mel exception.
If the mel creation operation is successful, the requesting task has now a functional mel variable with mel properties set to support subsequent mel operations. If the mel creation operation fails, the remote mel element is not allocated, but a resident mel variable is still created to allow the program to check for errors.

The mel variable created after a mel creation operation (either successful or failed), has the following initial values:

Mel Variable Creation Initial Property Values
PropertyValueSynopsis
IN Properties
<priority>0 CHAOS sets the priority property to 0 (starting default).
<timeout>0 CHAOS sets the timeout property to 0 (starting default).
OUT Properties
<value>null The <value> property will be set later by a read operation.
<count>0 The <count> property will be set later by a R/W or <snapshot> operation.
<sequence>0 The <sequence> property will be set later by a R/W or <snapshot> operation.
<status>set The <status> property is NERW_STATUS_OPEN if the mel creation is successful, and NERW_STATUS_CLOSED if the operation has failed.
<condition>0 The <condition> property will be set later by a R/W mel operation.
<error>set The <error> property is set to NERW_ERROR_NONE if the mel creation is successful, or NERW_ERROR_FAILED if the operation has failed.
<why>set The <why> property is null if the mel creation is successful, or contains the reason of the operation failure.
SET Properties
<buffer>set The <buffer> property is set to 1 by default, or to the value of the buffer attribute if specified.
<location>set The <location> property is set by the CHAOS runtime, or to the value of the at attribute if specified.
<readers>-1 The readers subscriptions property is set to -1 for undefined, since no reader task has yet accessed this mel element.
<writers>-1 The writers subscriptions property is set to -1 for undefined, since no writer task has yet accessed this mel element.
CORE Properties
<id>set The <id> property is set by the CHAOS runtime at creation time, and stays the same during the life of the mel element.
<url>set The <url> property is set by the CHAOS runtime at creation time, and stays the same during the life of the mel element.



Mel Passing Operation

Once a mel variable has been created, it can be used in the context of the creating task for any mel operation towards the mel element. If this task forks other tasks to run in parallel, and these tasks also need to access the same mel element, the forking parent task can pass its mel variable to the forked children tasks so that all the involved tasks refer to the same mel element.

Let's revisit the Producer/Consumer example:
main () {
     <mel> int store;
     <!> Producer (store);
     <!> Consumer (store);
}
void Producer (<mel> int store) {
     if ( store<error> ) {
          printf ("Mel [store] invalid due to [%s]", store<why>);
          return;
     }
     while ( <?>store = Produce() );
}
void Consumer (<mel> int store) {
     if ( store<error>  ) {
          printf ("Mel [store] invalid due to [%s]", store<why>);
          return;
     }
     while ( 1 ) {
         DoConsume (store);
         printf ("Status in Consumer: %d", store<status>);
         if ( store<value> == 0 ) break;
     }
}
function DoConsume (<mel> int store) {
     Consume (<?>store);
     printf ("Status in DoConsume: %d", store<status>);
}
There is one mel element store, but three different mel variables. The main task creates the first mel variable, and then passes it to the Producer and Consumer tasks. Each of these children tasks creates its own mel variable whose properties having their initial values either reset to default or copied from the parent task. The following table shows how the properties are initialized in the mel variable resident in a child task:

Mel Argument Passing Initial Property Values
PropertyValueSynopsis
IN Properties
<priority>Reset CHAOS resets the priority property to 0 (starting default).
<timeout>Reset CHAOS resets the timeout property to 0 (starting default).
OUT Properties
<value>Copied The <value> property is copied from the parent task.
<count>Copied The <count> property is copied from the parent task.
<sequence>Copied The <sequence> property is copied from the parent task.
<status>Copied The <status> property is copied from the parent task.
<condition>Copied The <condition> property is copied from the parent task.
<error>Copied The <error> property is copied from the parent task.
<why>Copied The <why> property is copied from the parent task.
SET Properties
<buffer>Copied The <buffer> property is copied from the parent task.
<location>Copied The <location> property is copied from the parent task.
<readers>Copied The readers subscriptions property is copied from the parent task.
<writers>Copied The writers subscriptions property is copied from the parent task.
CORE Properties
<id>Common The id property is fixed by the CHAOS runtime at creation time, and is common to all mel variables.
<url>Common The url property is fixed by the CHAOS runtime at creation time, and is common to all mel variables.

As the child task conducts its own mel operations towards the mel element, the property values of its resident mel variable will start to diverge from the ones maintained by the parent task and by other child tasks.

Passing By Value

Except for a few properties (<priority> and <timeout>) which are reset, all the other properties have their values copied from the parent task's properties. This is why in NERWous C, mel variables are said to be passed by value between tasks.

The above example shows the pass-by-value in action with the validity checking of the mel variable. If the main task has failed to create the mel element, the <error> property of the eponymous mel variable in the main task context will be set to a non-0 error code and the <why> property contains the description of the error. When the main task forks the Producer and Consumer tasks, the <error> and <why> properties are copied from the main task to the mel variables resident in the Producer and Consumer tasks. These tasks use these valued properties to do the error checking. If the failure checking returns positive, both tasks will end right away without doing their assigned work.

Short Passing By Value

Many times a child task receives all the values of a mel variable passed over by a parent task, but does not care about the stale <value> property that the parent task has cached from its earlier operations against the mel element. This <value> property can be substantial in size, and requires more time to transfer over the nerw network, impacting the performance of the program.

The only information that all child tasks demand from the parent task are those properties that help them connect to the remote mel element. When the parent task just passes this minimum requested properties, the pass is called a short pass. A programmer can specify short passing by using the short attribute:
main () {
     <mel> int store;
     <!> Producer (store);
     <!> Consumer (store);
}
void Producer (<mel pass=short> int store) {
     if ( store<error> ) {
          printf ("Mel [store] invalid due to [%s]", store<why>);
          return;
     }
    while ( <?>store = Produce() );
}
void Consumer (<mel short> int store) {
     if ( store<error> ) {
          printf ("Mel [store] invalid due to [%s]", store<why>);
          return;
     }
     while ( 1 ) {
         DoConsume (store);
         printf ("Status in Consumer: %d", store<status>);
         if ( store<value> == 0 ) break;
     }
}
function DoConsume (<mel> int store) {
     Consume (<?>store);
     printf ("Status in DoConsume: %d", store<status>);
}
The Producer and Consumer functional tasks now explicitly request short passing for the mel variable argument store. The attribute keyword pass is optional -- while Producer mentions it, Consumer skips it.

These are the initial values of a short-passed mel variable on a child task:

Short Passed Mel Argument Initial Property Values
PropertyValueSynopsis
IN Properties
<priority>Reset CHAOS resets the priority property to 0 (starting default).
<timeout>Reset CHAOS resets the timeout property to 0 (starting default).
OUT Properties
<value>Reset CHAOS resets the <value> property to null.
<count>Reset CHAOS resets the <count> property to 0.
<sequence>Reset CHAOS resets the <sequence> property to 0.
<status>Copied The <status> property is copied from the parent task.
<condition>Copied The <condition> property is copied from the parent task.
<error>Copied The <error> property is copied from the parent task.
<why>Copied The <why> property is copied from the parent task.
SET Properties
<buffer>Copied The <buffer> property is copied from the parent task.
<location>Copied The <location> property is copied from the parent task.
<readers>Copied The readers subscriptions property is copied from the parent task.
<writers>Copied The writers subscriptions property is copied from the parent task.
CORE Properties
<id>Common The id property is fixed by the CHAOS runtime at creation time, and is common to all mel variables.
<url>Common The url property is fixed by the CHAOS runtime at creation time, and is common to all mel variables.

As seen in the table above, the difference between a mel argument regular passing and short passing is the properties related to <value>, such as the <value> property itself and the related properties of <count> and <sequence>.
Side note: The use of short passing above does not much change the performance of our current example because as the parent task, the main task does not make a read to the mel element and therefore still has a null <value> property in its store mel variable.

It is possible that a NERWous compilation system includes an optimizer that analyzes functional task codes and automatically imposes an implicit short passing if the tasks do not make use of the <value>, <count> nor <sequence> property before a read, write or snapshot operation to the mel element. With such optimizer in place, there will be no need to make the code more verbose with explicit short attributes.

Passing By Reference

While mel variable passing between tasks is by value, mel variable passing between functions within a task is by reference. A task has only one mel variable to represent the communication channel between itself and the remote mel element. If the task calls sequential functions that refer to that mel element, these functions will have the resident mel variable passed by reference, forcing all the code within the task to share the same resident mel variable. In the Consumer task above, the function DoConsume updates the same mel variable, and that update is reflected back in the Consumer code block -- both printf the same <status> value.

No Pointers

It is worth mentioning again that there is no concept of a pointer to a mel variable. Between tasks, mel variables are always passed by value, and within tasks, mel variables are always used by reference.


Mel Operations Error Codes

Since a mel operation is a run time activity, it can fail. In NERWous C, such failure can be detected by the programmer by checking the <error> property of the mel variable immediately after a mel operation. When a task requests something to be done with a mel element, it sends the request to the CHAOS runtime via the mel variable. The CHAOS runtime sets the <error> property to 0, carries the operation, and reports any error code back in the same <error> property. If the <error> property remains 0, the operation has been successful. Otherwise, the operation has failed somehow, and the error code of the failure is reflected in the <error> property.

The type of the <error> property is an int. If a programmer wants to work with these error codes specifically (instead of just 0 or not 0), the NERWous C include file nerw.h has the predefined constants.

ExceptionsError CodesSynopsis
NERW_ERROR_NONEThere is no error raised against this successful mel operation.
TIMEDOUTNERW_ERROR_TIMEDOUTThe TIMEDOUT error occurs when the mel operation takes longer than specified.
CLOSEDNERW_ERROR_CLOSEDThe CLOSED error occurs when the mel element has been closed.
NOREADERSNERW_ERROR_NOREADERSThe NOREADERS error occurs when a write operation is requested to a mel element that does not have any subscribing readers.
NOWRITERSNERW_ERROR_NOWRITERSThe NOWRITERS error occurs when a read operation is requested to a mel element that does not have any subscribing writers.
...NERW_ERROR_FAILEDThis is a concatenation of all errors, including errors listed above and undocumented errors.

The value of NERW_ERROR_NONE is 0. The value of NERW_ERROR_FAILED is NERW_ERROR_TIMEDOUT | NERW_ERROR_CLOSED | NERW_ERROR_NOREADERS | NERW_ERROR_NOWRITERS | other undocumented errors. Thus the following statements are equivalent:
if ( store<error> ) printf ("error found");
if ( store<error> != NERW_ERROR_NONE ) printf ("error found");
if ( store<error> & NERW_ERROR_FAILED ) printf ("error found");
To check for a particular error, such as a NERW_ERROR_TIMEDOUT, use the following statements are equivalent:
if ( store<error> & NERW_ERROR_TIMEDOUT ) printf ("timeout error found");
if ( store<error> == NERW_ERROR_TIMEDOUT ) printf ("timeout error found");
The CHAOS runtime will abort an operation on the first error. Since it does not try to find all the possible errors and accumulate the error codes, checking the <error> attribute with bit-wise "and" operator or the equality operator are equivalent. The equality operator checking allows the use of the C switch statement, as will be seen in the Errors in Writes example later.

Errors In Creation

This is an example of using <error> to check the result of a mel creation operation:
main () {
    <mel> int store1, store2;
    if ( store1<error> ) {
        printf ("Failed to create [store1]: [%d] due to [%s]",
            store1<error>, store1<why>);
        exit 411;
    }
    <!> DoubleStoreProcessor(store1, store2);
}
void DoubeStoreProcessor (<mel> in store1, <mel> int store2) {
    if ( store2<error> ) {
        printf ("Failed to create [store2]: [%d] due to [%s]",
            store2<error>, store2<why>);
        return;
    }
    processStore (store1);
    processStore (store2);
}
The task main creates two mel variables, store1 and store2. It handles the error checking for store1, but passes the error checking for store2 to the forked child task, DoubleStoreProcessor.

Errors In Reads

This is an example of using <error> in a mel read operation, with specific mel error codes:
#include <nerw.h>
void Consumer (<mel> int store) {
    while ( 1 ) {
        <? timeout>store;
        if ( store<error> & NERW_ERROR_TIMEDOUT ) {
            printf ("Cannot wait longer. Do something else. ");
            doSomethingElse ();
            printf ("Now try again. ");
            continue;
        }
        else if ( store<error> & NERW_ERROR_FAILED ) {
            printf ("Give up on error [%d] due to [%s],
                    store<error>, store<why>);
            break;
        }
        else if ( store<value> == 0 ) {
            return;     /* stop consumption on 0 value */
        }
        else {
            Consume (store<value>);
            continue;
        }
    }
}
This time we need to include the nerw.h because we are itemizing some of the error codes. This read operation includes a timeout attribute to break a mel wait if the mel element remains empty for too long. Note the use of the <value> property if the mel read is successful, and the <why> property if the read is unsuccessful due to failures besides timeout.

Errors In Writes

This is an example of using <error> in a mel write operation:
void Producer (<mel> int store) {
    int c;
    while ( ( c = Produce() ) ) {
        <? timeout>store = c;
        switch ( store<error> ) {
          case 0: case NERW_ERROR_NONE:
            break;
          case NERW_ERROR_TIMEDOUT:
            printf ("Cannot wait longer. Do something else . ");
            doSomethingElse ();
            printf ("Now try again. ");
            break;
          default:
            printf ("Give up on error [%d] due to [%s],
              store<error>, store<why>);
            return;
        }
    }
}
The Producer task keeps Produce'ing up until a 0-valued product. With a non-0 product, the task sends it to the mel element via a <?>= write statement. Like the Consumer task, this Producer also makes use of the timeout attribute to limit its wait in case the mel buffer remains full for too long, preventing a new product deposit.
Side note: An astute programmer will notice that in the NERW_ERROR_TIMEDOUT case, the break statement will cause the rerun of the Produce function in the while loop, which results in a new product and the loss of the existing product that got timed out. If we don't want to loose the existing product that fails to be written due to the timeout, we can use the NERWous C <resume> operation which attempts to redo the timed-out write operation with the existing product.

No Errors

If the mel operation is successful, the CHAOS runtime will set the <error> property to NERW_ERROR_NONE.


Mel Operations Error Descriptions

When a mel operation failure is detected, the CHAOS runtime sets the <error> property with an error code, and tries to collect more information about the failure. Sometimes there is none, most often the underlying environment will provide a fair amount of information. Whatever CHAOS can collect will be written into the <why> property, overwriting any previous value.

The following example shows a Consumer that collects all the errors during its read access attempts to the mel element:
void Consumer (<mel> int store) {
    char allerr[HUGE];
    while ( 1 ) {
      allerr[0] = '\0';
         int c = <?>store;
         if ( !c ) break;
         if ( store<error> ) {
             strcat (allerr, store<why>);
         else
             Consume (c);
    }
    printf ("All errors [%s]", allerr);
}
The above consumer uses the local allerr to concatenate all the <why> values since a later failure will override the reason of a previous failure in the <why> property.

Before a mel operation, the <why> property is set to an empty string. If the mel operation is successful, the <why> property remains an empty string. If an error has happened, CHAOS will update the <why> property with the cause of the error. If CHAOS runtime cannot collect additional detail about the failure from the underlying environment, it will update the <why> property with a generic note such as "Error detected". Thus, an empty-stringed <why> property can be used to check if an operation is successful or not.


Mel Operations Exceptions

Let's revisit the Consumer task where we check for different error codes.
#include <nerw.h>
void Consumer (<mel> int store) {
    while ( 1 ) {
        <? timeout>store;
        switch ( store<error> ) {
          case NERW_ERROR_NONE:
            if ( store<value> == 0 )
                return;     /* stop on 0 value */
            Consume (store<value>);
            break;
          case NERW_ERROR_TIMEDOUT:
            printf ("Cannot wait longer. Do something else. ");
            doSomethingElse ();
            printf ("Now try again. ");
            <resume>;    /* redo the mel wait */
          case NERW_ERROR_CLOSED:
            printf ("Stop since the mel access has been closed");
            return;
          default:
            printf ("Give up on error [%d] due to [%s],
                store<error>, store<why>);
            return;
        }
     }
}
If there is no error (NERW_ERROR_NONE), the Consumer task checks if the just read product is the last one (e.g. it has a 0 value, per convention with the Producer task). If it is, the task ends; otherwise it consumes the read product. If the wait for the product takes longer than the default timeout (NERW_ERROR_TIMEDOUT), the task runs doSomethingElse and <resume> the wait on the mel element. This Consumer task also adds a check for the mel being closed (NERW_ERROR_CLOSED), so that it can end itself. Other errors are handled in the default case where the task displays all the information it has about the error before calling return to end itself.

Let's rewrite the above example using exceptions:
#include <nerw.h>
void Consumer (<mel> int store) {
    while ( 1 ) {
      try {
          Consume (<? timeout>store);
      }
      catch ( store<TIMEDOUT> )  {
          printf ("Cannot wait longer. Do something else. ");
          doSomethingElse ();
          printf ("Now try again. ");
          <resume>;
      }
      catch ( store<CLOSED> )  {
          printf ("Stop since the mel access has been closed");
          return;
      }
      catch ( store<...> )  {
          printf ("Give up on error [%d] due to [%s],
              store<error>, store<why>);
          return;
      }
    }
}
The list of exceptions corresponds to the list of error codes. A mel exception, such as store<TIMEDOUT> or store<CLOSED>, is specified within the < > markers, and placed after a mel variable, like a mel property. Unlike mel properties which are lower cased, mel exceptions are upper cased.

To follow the footsteps of C derivative languages (such as C++ or C#), NERWous C exceptions are handled by the try/catch construct. With this construct, all the good behaviors can be concentrated in the try section allowing the main logic of the program to be followed without error-checking interruptions; and all the bad behaviors are delegated into various catch sections to ensure that the program is properly strengthened for production runs. For example, let's look at the following code snippet:
    while ( 1 ) {
      try {
          Consume (<? timeout>store1);
          Consume (<?>store2);
      }
      catch ( store1<TIMEDOUT> )  {
          printf ("Cannot wait longer. Do something else. ");
          doSomethingElse ();
          printf ("Now try again. ");
          <resume>;
      }
      catch ( store1<...> )  {
          printf ("Give up on [store1] due to error [%d] with cause [%s],
              store1<error>, store1<why>);
          return;
      }
      catch ( store2<...> )  {
           printf ("Give up on [store2] due to error [%d] with cause [%s],
               store2<error>, store2<why>);
          return;
      }
    }
}
We can easily see that the logic of the code snippet is to Consume the values of two mel elements, with all the error handling listed in the catch sections, some for the mel variable store1, and some for store2. Note that the <TIMEDOUT> exception is only for store1 since its read operation requests a timeout instead of waiting indefinitely as in store2.

Catch-All Exception

The exception <...> is called the catch-all exception. Any exception that is not specifically handled by an explicit catch section goes to the catch-all exception. In the list of exceptions and error codes, the catch-all exception is listed in the same row as the error code NERW_ERROR_FAILED.

There are two types of catch-all handlers: the single mel catch-all and the all mel catch-all.

Single Mel Catch-All

Two examples of the single mel catch-all handler are found in the previous sample:
catch ( store1<...> )
and
catch ( store2<...> )
This type of handler matches the identified mel with all possible exceptions. To find what error triggers the exception to that mel, the <error> and <why> properties are used, such as in the code example above:
catch ( store1<...> ) {
    printf ("Give up on [store1] due to error [%d] with cause [%s],
        store1<error>, store1<why>);
    return;
}

All Mel Catch-All

This type of handler matches all mel elements with all possible exceptions. To find what mel triggers the exception, the as attribute and the <name> property are used. To find what error against that mel causes the exception, the <error> and <why> properties are used:
catch ( <...> as store ) {
    printf ("[%s] encounters error [%d] due to [%s],
        store<name>, store<error>, store<why>);
}
The store mel variable is called a stand-in variable. Any unique name can be used, but whatever that name is, it can only be used inside the handler to represent the actual mel that causes the exception. We will see the as attribute in play in a subsequent example.

Exception Handler Matching

Once an error happens to a mel operation, CHAOS will find a matching handler to process that exception. The matching follows these rules:
  1. The handler must specify the mel under exception
  2. The handler must cover the exception
  3. If multiple handlers match, the more restrictive one is selected.
For example in the previous code snippet, if store1 wait gets timed out, the handler catch ( store1<TIMEDOUT> ) gets selected since it satisfies rules 1 and 2, and is more restrictive than catch ( store1<...> ).

We will revisit the restrictive rule in the chapter on combined reads where we introduce AND and OR combination handlers that vie with the single handlers we see here for matching selection.

Exception Escalation

When an exception occurs, CHAOS will attempt to find a matching exception handler associated with the mel operation. If there is no matching catch handler, CHAOS will escalate the exception within the task. For example, many tasks are written with an envelop try / catch (...) so that there is a last-resort catch-all handler to catch any un-matched exception, and allow the task to end graciously. If there is no matching handler within the task, CHAOS will abort the task.

Let's take a look at this Consumer example:
void Consumer (<mel> int store1, <mel> int store2) {
  try {
    while ( 1 ) {
      try {
          Consume (<? timeout>store1);
          Consume (<?>store2);
      }
      catch ( store1<TIMEDOUT> ) {
          printf ("Cannot wait longer. Do something else. ");
          doSomethingElse ();
          printf ("Now try again. ");
          <resume>;
      }
      catch ( store1<...> ) {
          printf ("Give up on [store1] due to error [%d] with cause [%s],
              store1<error>, store1<why>);
          break;
      }
    }
    return;    /* normal task ending */
  }
  catch ( <...> as store )  {
    printf ("[%s] encounters error [%d] due to [%s],
       store<name>, store<error>, store<why>);
    return;   /* graceful task ending */
  }
}
Inside the while loop, the above Consumer has a specific handler for the TIMEDOUT exception on store1 and a catch-all handler for any other store1 exception. It does not have any handler for store2. If there is an exception on store2 during the read statement
<?>store2
CHAOS will force Consumer to exit the while loop to look for a matching handler. In fact, there is one. The envelop catch-all handler
catch ( <...> as store )
matches all exceptions of all mels, and will process the store2 exception. Using the as attribute (with store as the stand-in mel), this handler can display store2 name, the error code and the reason for the error.

If there is no matching handler within the task, CHAOS will end the task abnormally, generating a pel exception. This exception will be caught by any task doing a pel wait on the abnormally ending task. Right now we are focusing on memory elements (mels) and their exceptions. Processing elements (pels) and related exceptions will be the topics of future chapters.

Exceptions Clearing

Once an exception is matched with a handler and processed, the exception is cleared. If the mel operation is inside a loop, the exception from the last iteration will not cause a jump into a handler on the next iteration. Let's look at the following example:
void Consumer (<mel> int store) {
    while ( 1 ) {
      try {
          Consume (<? timeout>store);
      }
      catch ( store<TIMEDOUT> ) {
          printf ("Cannot wait longer. Do something else. ");
          doSomethingElse ();
          printf ("Now try again. ");
      }
      catch ( store<CLOSED> ) {
          printf ("WARNING: the mel access has been closed");
      }
      continue;   /* loop back */
    }
}
The above Consumer task runs an infinite loop. If during an iteration, the mel store gets longer to be valued, and a TIMEDOUT exception occurs, the TIMEDOUT handler will be invoked. On jump-out of the handler, the TIMEDOUT exception is cleared. The continue statement loops back the while loop, to wait for the mel store anew. Any TIMEDOUT exception now is due to this wait, and not from the previous mel wait.

If the mel store has been closed, future iterations will get the CLOSED exception again, and the warning message keeps getting printf'ed. However this is not due to the previous CLOSED exception not being cleared, but due to CHAOS detecting that the mel has been closed on the new mel wait. In other words, while the CLOSED exception has been cleared on jump-out of the handler, the state of the mel being closed still remains, causing recurring jump-ins into the handler. The programmer should have a break statement at the end of the CLOSED handler to get off the while loop.

Exceptions vs. Errors

When a failure occurs, the mel operation is aborted and an error is generated. If the error is within a try / catch construct, an exception is generated, and the exception escalation process takes over. If the mel operation is not wrapped with a try / catch construct, exception handling does not come into play, and code fortification will depend solely on <error> property checking.

Let's repeat this earlier example:
main () {
    <mel> int store1, store2;
    if ( store1<error> ) {
        printf ("Failed to create [store1]: [%d] due to [%s]",
            store1<error>, store1<why>);
        exit 411;
    }
    <!> DoubleStoreProcessor(store1, store2);
}
void DoubeStoreProcessor (<mel> in store1, <mel> int store2) {
    if ( store2<error> ) {
        printf ("Failed to create [store2]: [%d] due to [%s]",
            store2<error>, store2<why>);
        return;
    }
    processStore (store1);
    processStore (store2);
}
The task main creates two mel variables, store1 and store2. It handles the error checking for store1 creation failures, but passes the error checking for store2 to the forked child task, DoubleStoreProcessor.

Let's now modify the main task by wrapping its code within a try / catch envelop:
main () {
  try {
    <mel> int store1, store2;
    if ( store1<error> ) {
        printf ("Failed to create [store1]: [%d] due to [%s]",
            store1<error>, store1<why>);
        exit 411;
    }
    <!> DoubleStoreProcessor(store1, store2);
  }
  catch (<...> as store) {
      printf ("Failed to create [%s]: [%d] due to [%s]",
          store<name>, store<error>, store<why>);
      exit 911;
  }
}
If store1 fails to be created, the code block of if ( store1<error> ) { ... } with a 411 exit, is never entered again. Instead, the exception escalation process takes over, and the program flow jumps into the catch-all handler with a 911 exit.

If store2 fails to be created, the failure is not propagated to DoubleStoreProcessor. Instead, the exception handler of the main task takes over, and the program flow again flows into the catch-all handler with the 911 exit.

Adding a try / catch structure does not change the positive flow of the main task, when everything flows flawlessly. It does change the negative flow when there is an error happening. Any code that is introduced by or depends on checking the <error> property may be subsumed by an exception handling code.


Mel Snapshot Operation

The mel properties of a mel variable are automatically updated after a mel operation, and then cached within the context of the requesting task until the task makes another mel operation request. Between this time, the remote mel element has probably changed due to its interactions with other tasks. To capture these changes without doing an intrusive mel operation, an inquisitive task can invoke the non-intrusive <snapshot> operation.

Snapshot Updated Properties

The following table shows what properties will be updated by <snapshot>:

PropertyUpdated?Synopsis
IN Properties
<priority>Yes CHAOS resets the priority property to 0 since the <snapshot> operation does not run with a priority.
<timeout>Yes CHAOS resets the timeout property to 0 since the <snapshot> operation does not run with a timeout.
OUT Properties
<value>Yes The top value at the reader's end of the mel buffer is returned, without that value being removed from the buffer as in a read operation. CHAOS sets this property to NULL if the mel element is EMPTY.
<count>Yes If the returned <value> is an array, the number of items in the array is reflected in the <count> property.
<sequence>Yes If <value> is updated, this property contains its version identifier. Otherwise CHAOS resets it to 0.
<status>Yes Current state of the mel element
<condition>Yes Current state of the mel element (same as <status>)
<error>Yes Error code of the <snapshot> operation. If the operation is successful, CHAOS sets this property to NERW_ERROR_NONE.
<why>Yes Failure reason of the <snapshot> operation. If the operation is successful, CHAOS resets this property to an empty string.
SET Properties
<buffer>Yes A <rebuffer> operation may have resized the mel buffer.
<location>Yes The current CHAOS-defined locality of the mel element. On platforms with high reliability, this value may have changed if the original locality has encountered problems and the platform fails over to another locality.
<readers>Yes The number of readers subscriptions may have changed.
<writers>Yes The number of writers subscriptions may have changed.
CORE Properties
<id>No This property is fixed by the CHAOS runtime.
<url>No This property is fixed by the CHAOS runtime.

A <snapshot> operation looks similar to a readonly operation, with some big differences. The <snapshot> request goes to the snapshot queue which is the highest priority queue of the mel element, and this request does not wait for the mel element to be FILLED -- if there is no value to be read, it returns right away with a NULL value for the <value> property, and the NERW_STATUS_EMPTY for the <status> and <condition> properties. On the other hand, the readonly operation goes to the readonly queue which is at a lower priority than the snapshot queue, and it waits until there is a value in the mel buffer to be read (or until the specified timeout occurs).

A Snapshot Monitoring Example

The simplistic task below illustrates the use of <snapshot> to monitor the state of a mel element.
void Monitor (<mel> int store) {
   while (1) {
      <snapshot> store;
      if ( store<status> &  NERW_STATUS_EMPTY ) {
          printf ("The mel buffer is empty\n");
          printf ("A reader task will likely have to wait\n");
      }
      else {
          printf ("The mel buffer contains a value\n");
          printf ("A reader task will likely not have to wait\n");
      }

      if ( store<status> &  NERW_STATUS_FULL ) {
          printf ("The mel buffer is full\n");
          printf ("A writer task will likely have to wait\n");
      }
      else {
          printf ("The mel buffer has an available slot\n");
          printf ("A writer task will likely not have to wait\n");
      }

      if ( store<status> &  NERW_STATUS_CLOSED ) {
          printf ("The mel element is closed\n");
          printf ("All tasks will have their mel requests failed\n");
      }

      sleep (60000);    /* wait 1 min and check again */
   }
}
The above Monitor is a sampling monitor. Between the one-minute gaps, there could be writers that deposit new values to the mel store, and readers to take them out. The Monitor only captures the state of the mel element at its one-minute marks.

Snapshot Operation Attributes

The <snapshot> operation has only one attribute:

OperationAttributesSynopsis
<snapshot> store Mel snapshot operation
on
=store2
Duplicate the properties of store2 onto store.

The <snapshot> default operation (without the on attribute) re-initializes the given mel variable with values direct from the associated remote mel element. When used with the on attribute, the <snapshot> operation initializes (or re-initializes) the given mel variable with property values from another mel variable.

The mel variable argument to on must be in scope of the invoking task. This means that it can only be a local mel variable resident in the same task, or a global variable known by all tasks. What it cannot be is a local mel variable of another task.

The on attribute is seldom used explicitly in a program code. It is available for behind-the-scene support for the <extern> statement for global mel variables.

It is worth reminding that NERWous C does not support pointers to mel variables. The <snapshot> on duplicates the properties of a mel variable (most importantly the CORE properties), resulting in two separate mel variables associating themselves to the same remote mel element.


Previous Next Top

Wednesday, June 20, 2018

GPU Programming with CUDA

Welcome » NERWous C » Examples
  1. Current Literature
  2. NERWous C Sample


Current Literature

CUDA is the parallel computing platform and programming model from NVIDIA. It allows programs to run on the main Central Processing Unit (CPU) and offload heavily mathematical computation tasks to NVIDIA Graphical Processing Units (GPUs). A computer host can have one or more CPUs and one or more GPUs. All these computer resources can be harnessed by the CUDA parallel programming model.

Let's take a look at this CUDA example that does addition of two arrays of 1 million elements:
#include <iostream>
#include <math.h>

#define NUMBLOCKS 4
#define NUMTHREADSPERBLOCK 256

// Kernel function to add the elements of two arrays
__global__
void add(int n, float *x, float *y)
{
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; 
       i < n; 
       i += blockDim.x * gridDim.x)
    y[i] = x[i] + y[i];
}

int main(void)
{
  int N = 1<<20;
  float *x, *y;

  // Allocate Unified Memory – accessible from CPU or GPU
  cudaMallocManaged(&x, N*sizeof(float));
  cudaMallocManaged(&y, N*sizeof(float));

  // initialize x and y arrays on the host
  for (int i = 0; i < N; i++) {
    x[i] = 1.0f;
    y[i] = 2.0f;
  }

  // Run kernel on 1M elements on the GPU
  // on 4 thread blocks with 256 threads per block for a total of 1024 threads
  add<<<NUMBLOCKS, NUMTHREADSPERBLOCK>>>(N, x, y);

  // Wait for GPU to finish before accessing on host
  cudaDeviceSynchronize();

  // Check for errors (all values should be 3.0f)
  float maxError = 0.0f;
  for (int i = 0; i < N; i++)
    maxError = fmax(maxError, fabs(y[i]-3.0f));
  std::cout << "Max error: " << maxError << std::endl;

  // Free memory
  cudaFree(x);
  cudaFree(y);
  
  return 0;
}
The specifier __global__ to the function ''add'' tells the CUDA C++ compiler that "this is a function that runs on the GPU and can be called from CPU code".

The cudaMallocManaged CUDA function allocates memory for the shared arrays from the Unified Memory space. This memory space is accessible to both the main CPU and the GPU.

The above CUDA example uses a technique called grid-stride loop. Instead of running the add function for 1M items in 1M virtual threads, it runs based on the capability of the hardware on hand. While programming with the unlimited threads assumption results in pretty concise code, in practice, the cost of setting up and tearing down actual processing units to map to virtual threads imposes a performance penalty on the running program. The grid-stride loop strategy identifies the number of available executing units, and then divides the work to these units. In the above CUDA example, NUMBLOCKS * NUMTHREADSPERBLOCK units (or threads in CUDA terminology) divvy up the 1 M addition tasks, with each thread handle 1 M / NUMBLOCKS * NUMTHREADSPERBLOCK additions.

NERWous C Sample

This is the NERWous C sample correspondent:
#define NUMBLOCKS 4
#define NUMTHREADSPERBLOCK 256
#define NUMTHREADS	NUMBLOCKS*NUMTHREADSPERBLOCK

void add(int n, mel float *x, mel float *y)
{
   <cel> mythread = pel<location>;
   int index = atoi(mythread<tag>);
   int stride = NUMTHREADS;
   
   for (int i = index; i < n; i += stride)
      y[i] = x[i] + y[i];
}

main () {
   int N = 1<<20;    /* 1 M items */
 
   // Allocate Unified Memory – accessible from CPU or GPU
   <mel at=UnifiedMemory> float x[N];
   <mel at=UnifiedMemory> float y[N];
 
   // Initialize x and y arrays on the host
   <?> (x, y) {
      for (int i = 0; i < N; i++) {
         x[i] = 1.0f;
         y[i] = 2.0f;
      }
   }

   // Run kernel on 1M elements on the GPU
   <cel> threads[NUMTHREADS];
   
   <collect> for (int index = 0; index < NUMTHREADS; ++index)
   <! at=threads[index]> {
      add (N, x, y, index, numthreads);
   } <? ENDED>;    // Wait for GPU to finish before accessing on host
   
   // Check for errors (all values should be 3.0f)
   float maxError = 0.0f;
   <?>(y) {
      for (int i = 0; i < N; i++)
         maxError = fmax(maxError, fabs(y[i]-3.0f));
   }
   printf ( "Max error:  %d", maxError);   
   
   // Free memory
   <close> x;
   <close> y;
   
   return 0;
}
The CUDA example identifies NUMBLOCKS*NUMTHREADSPERBLOCK threads. These are defined in NERWous C as cel array variable threads:
<cel> threads[NUMTHREADS];
The for (int index = 0; index < NUMTHREADS; ++index) loop divvies the 1-Million N items into the available threads cels. By wrapping the for loop with the collect wrapper, the main task waits for all additions to be finished before it checks for errors. This is equivalent to the CUDA library function cudaDeviceSynchronize.

The additions are done by the function add. Instead of doing an addition for one matrix element, it runs a for loop to handle N/NUMTHREADS items. These items are not consecutive. For example, the add task running on the cel threads[0] will handle item 0 then item NUMTHREADS then item NUMTHREADS*2, then item NUMTHREADS*3, and so on.

The add task uses, as its starting index into the matrix array, the index of the cel threads it is running on. It finds this information by first locating the hosting cel via the pel<location>, and then finding the tag of the cel via mythread<tag>. The tag value is a string and must be converted to an integer to be used as a numerical index. For an array of cels, such as <cel> threads[NUMTHREADS], the default tag value goes from the string "0" to the string for NUMTHREADS-1. In other environments, the tag value may be a non-numerical string to reflect the underlying cataloging scheme.

The matrixes x and y are shared entities accessible by all add tasks. In the CUDA environment, they are hosted in the Unified Memory. This space is represented by the cel UnifiedMemory used in the declaration of the mel variables x and y. A NERWous C translator for the CUDA environment would translate those definitions into the CUDA cudaMallocManaged and other related library functions.


Previous Next Top

Saturday, April 14, 2018

Mel Mixed Zones

Welcome » NERWous C » Mel
  1. Mixed LIST Zones
  2. Checkout Operation
  3. Mixed AND Zones
  4. Mixed OR Zones


Mixed LIST Zones

Now that we have got to know individually both the mel reader zones and writer zones, it is time to mix them up, by requesting both of them.

Let's start with this combination of a mel read and mel write, where we are transferring the value from one mel variable to another:
/* VERSION 1 */
<mel> int a, b;
<?>b = <?>a;
We now change the code to make use of exclusive zones. We enter such a zone with the mel variable a on the reader side, and the mel variable b on the writer side:
/* VERSION 2 */
<?reader> a, <?writer> b {
    b = a;
}
The mode reader for the mel variable a zone wait is optional and can be omitted, although having an explicit reader visually counter-balances the mode writer which is required for the b writer zone wait:
/* VERSION 3 - compact but looks visually "unbalanced" */
<?> a, <?writer> b {
    b = a;
}
This is the behind-the-scene process:
  1. The task puts itself onto the mel variable a's reader queue, and onto the mel variable b's writer queue.
     
  2. On a's reader queue, whenever the mel reader zone conditions are satisfied, the task will hold on to a, preventing other reader tasks to read from a, even if there are values to be read in the mel buffer a. The task now has exclusive read access to a.
     
  3. On b's writer queue, whenever the mel writer zone conditions are satisfied, the task holds on to b, preventing other writer tasks to write to b, even if there are empty slots in the mel buffer b to receive the writings. The task now has exclusive write access to b.
     
  4. Once the task gets hold of both the required mels, the task checks into the mixed exclusive zone.
     
  5. In the exclusive zone, the task uses the local eponymous variables, a and b. The local eponymous variable a contains the value of the mel variable a initialized as part of the reader zone checkin. The local eponymous variable b is un-initialized since it comes from the writer zone checkin process which does not initialize the eponymous variable.
     
  6. Inside the exclusive zone, the local eponymous variable b is assigned the value of the eponymous variable a. The remote mel variables a and b are not touched.
     
  7. Once done with the work inside the exclusive zone, the task checks out of the zone. Since there is no special checkout instructions, the task does a default checkout. The mel variable a is emptied of its value, and the mel variable b receives a new deposit from the eponymous variable b.
     
  8. The task concludes the checkout process by removing itself from the reader queue of the mel variable a, and from the writer queue of the mel variable b.


Checkout Operation

The checkout for a mixed zone can take many forms.

Default Checkout

If there is no explicit checkout command (as in VERSION 2 above) or if the checkout statement was invoked without attributes (as in VERSION 4 below), the default checkout process for each zone will be carried out. For reader zones, it will be <checkout read>, and for writer zones, <checkout write>.
/* VERSION 4 - Checkout statement without attribute */
<?reader> a, <?writer> b {
    b = a;
    <checkout>;
}
Custom Checkout

The default checkout behavior can be customized for each mel zone wait:
/* VERSION 5 - Customized default */
<?reader+skip> a, <?writer> b {
    b = a;
}
The checkout for the mel variable b is still the checkout write process. However the default checkout for the mel variable a has been redefined to be the skip behavior. The mel variable a will not be emptied of its value. Instead this value is retained, and available for another reader task.

Explicit Checkout

The default checkout behaviors will be overridden if the checkout statement is used with an attribute:
/* VERSION 6 - Explicit checkout */
<?reader+skip> a, <?writer> b {
    b = a;
    <checkout skip>;
}
The skip behavior applies to both a and b. For the mel variable a, the explicit skip behavior matches the custom skip behavior, so there is no change. For the mel variable b, the explicit skip behavior overrides the default write behavior. Thus, the mel variable b will not be filled with the value of the eponymous local variable b.

The explicit checkout can be customized for each mel variable:
/* VERSION 7 - Customized explicit checkout */
<?reader+skip> a, <?writer> b {
    b = a;
    if ( b == 0 ) <checkout a=read b=skip>;
}
Here, if the value of the mel variable a is 0, not only we don't want to use it to fill the mel variable b, we also want to remove it from the mel variable a so that the next read will not have to see this 0 value. Since the default checkout behavior of a is skip, we have to override it back to the read behavior.


Mixed AND Zones

Let's get back to VERSION 2. There we used the LIST comma (,) separator between the mel zone waits. We can also use the AND (&&) separator:
/* VERSION 8 - Mixed AND zones */
<?reader> a && <?writer> b {
    b = a;
}
We still end up with the mixed zone having exclusive read access to a and exclusive write access to b. The difference is that the AND wait requires both the read conditions for a and the write conditions for b to be true at the same time before the task can check into the mixed zone. Compared with the LIST wait, it is usually harder to satisfy an AND wait (i.e. the task may have to wait longer for all the conditions to be aligned), but this will prevent deadlocks when multiple tasks vie for the same resources in a round-robin fashion.


Mixed OR Zones

Mixed zones can also support the OR zone waits:
/* VERSION 9 - Mixed OR zone */
<?reader> a || <?writer> b {
<case> a:
    <?b> = a;
    <checkout>;
<case> b:
    b = <?>a;
    <checkout>;
}    
This is the behind-the-scene process:
  1. The task puts itself onto the mel variable a's reader queue. If the mel reader zone conditions for a are satisfied, the task enters the mixed zone at the <case> a code block.
     
  2. If the mel zone read conditions for a are not satisfied, the task puts itself onto the mel variable b's writer queue. If the mel writer zone conditions for b are satisfied, the task enters the mixed zone at the <case> b code block.
     
  3. If neither set of conditions can be satisfied, the task will wait at both mel queues until one set of conditions is met. The task then gets off the other mel queue, and enters the mixed zone at the appropriate <case> code block.
     
  4. In both <case> a and <case> b code blocks, we want to write to the mel variable b the read value of the mel variable a. The code is different depending on which <case> code block we get to use.
     
  5. For <case> a, the value of the mel variable a is transferred to the eponymous local variable a. There is no eponymous local variable b for the mel variable b. To write to the mel variable b, the mel write wait construct <?>b must be used. In other words, the task has to contend with any other writer tasks for the write access to the mel variable b since it does not have exclusive access to b.
     
  6. For <case> b, the eponymous local variable b stands for the mel variable b. There is no eponymous local variable a. To access the mel variable a to read its value, the mel read wait construct <?>a must be used.
     
  7. Each <case> code block must end with a checkout statement. This statement can be explicit as in the above example, or implicit by running out of code in the code block. The implicit checkout will cover any missing explicit checkout statement. There is no code bleeding from one <case> code block to the next.

If the random OR behavior is specified, the task will randomize the zone wait check, instead of always checking a reader queue first then b writer queue:
/* VERSION 10 - Random OR wait */
<?reader> a ||<random> <?writer> b {
<case> a:
    <?b> = a;
    <checkout>;
<case> b:
    b = <?> a;
    <checkout>;
}    


Previous Next Top

Monday, February 12, 2018

2/12/2018 at the NERW

The NERW Concurrency Model is written as a blog. Articles are posted a little pell-mell. To read the NERW Concurrency Model logically, please click on the Welcome tab.

Today, 2/12/2108, when the XXIII Olympic Winter Games is at the fourth (or fifth) day at Pyeongchang, South Korea, I have finished updating the NERWous C examples with the latest thinking. Now is the time to put these latest thinking back to the blog articles that describe them. This is the process. First some features are thought out in early draft blog articles. Then I see how the features play out with the examples. Finally the early drafts are updated.

Tuesday, January 23, 2018

Mel Writes

Welcome » NERWous C » Mel
  1. Mel Write Attributes
  2. Mel Writer Mode
  3. Mel Write Waits
    1. Mel Write Process
    2. Mel Write Conditions
    3. Mel Write Buffers
  4. Mel Writeover Mode
  5. Mel OR Writes
    1. Serial OR Writes
    2. Random OR Writes
    3. OR Write Exceptions
  6. Mel AND Writes
    1. Serial AND Writes
    2. Parallel AND Writes
    3. InSimul AND Writes
  7. Mel LIST Writes


Mel Write Attributes

The other side of a mel read which we explore in the previous chapter, is a mel write. Let's consider these two mel write statements:
<?>store1 = c1;
<? priority timeout mode="writeover">store2 = c2;
The first statement is a simple mel write statement with no attributes. The second statement is a mel write statement with all the attributes stated:

OperationAttributesSynopsis
<?>store = c Mel write operation on the mel variable store
priority
=n
Access the mel element for writing with priority n
timeout
=msec
Wait for the mel buffer to have an empty slot for writing for msec milliseconds before aborting
mode
=setting
Request write access to the mel with this mode setting:
writer: request a write access (default)
writeover: request an overwrite access
method
=<keyword>
Method used for mel multiple writes: mel OR writes and mel AND writes

A mel write statement is not only about accessing the remote mel element, but also about waiting for the task's turn to access in one of the writer's queues, then waiting for an empty slot in the mel buffer from the writer's end, and finally depositing the new value to that available slot. The last action will allow any reader task that is waiting for the mel element to be filled, to read out the new value and make the mel buffer slot available again for a new write.

Instead of depositing a new value, a writer task can overwrite the last written (and still un-read) value in the mel buffer, by using the mode attribute writeover. This feature can be used by a producing task to keep improving the interim result until a reader task takes it away.


Mel Writer Mode

A mel write operation accesses a mel variable at the writer's end of the mel buffer. A fully descriptive mel write operation specifies the writer mel buffer access mode. For example to write to the mel variable store the value of the local variable c:
<? mode=writer> store = c;
The mode keyword can always be omitted:
<?writer> store = c;
Finally, the attribute value writer can also be omitted:
<?> store = c;
Due to the position of the mel variable on the left-hand side of the = operator, a mel write operation can be identified at compile time without the verbose use of the writer mel buffer access mode. However, when we come upon mel writer zones in the future, we'll see the writer mode is necessary to differentiate them from mel reader zones.


Mel Write Waits

In the following Producer/Consumer example, the Producer task makes use of the mel write access:
main () {
  <mel> int store;
  <!> Producer (store);
  <!> Consumer (store);
}
void Producer (<mel> int store) {
  while ( 1 ) {
    try { <?>store = Produce(); }
    catch ( store<TIMEDOUT> ) {
      printf ("Timeout - try again");
      <resume>;
    }
    catch ( store<CLOSED> ) {
      printf ("Store closed - get out");
      break;
    }
    catch ( store<...> ) {
      printf ("Unexpected error [%d] due to [%s]", store<error>, store<why>);
      break;
    }
  }
}
void Consumer (<mel> int store) {
  int maxconsump = 500;
  while ( maxconsump-- ) {
    Consume(<?>store);
  }
  <close>store;
}
This Consumer task only Consumes 500 items. Then it ends the while loop, closes the mel store, and ends itself.

The purpose of the Producer task is to keep Produce'ing the items for Consumer until the latter is done with consumption.

Mel Write Process

This is the behind-the-scene process for the statement
<? timeout>store = Produce();
  1. The Producer task Produces a new product and puts itself into one of many possible writers' queues of the mel store.
     
  2. The writers' queue Producer selects to stand in line is based on the priority of the mel write operation. Here, no priority attribute is explicit so the default writers' queue is used.
     
  3. If there are other writer tasks in the queue, the Producer task moves up the queue as the front tasks get off the queue, until it reaches the top position of the queue.
     
  4. At the top position, the Producer waits for the queue to have WRITE access to the mel store. In this example, Producer is the only writer task, there is no vying for the access from higher priority queues. Therefore, although the default writers' queue has the lowest priority to write to the mel buffer, it is granted WRITE access.
     
  5. If the mel buffer is vacant, the Producer task will deposit its product into the mel buffer from the writer's end, increases the sequence number, and gets off the mel store default writers' queue. The mel buffer is now filled. The slot with the new product eventually moves up the mel buffer to the top position on the reader's end and becomes available for retrieval by the Consumer reader task.
     
  6. If the mel buffer is fullProducer keeps the top position in the queue and waits until the mel buffer of store becomes vaant. Then it processes as above.
     
  7. If Producer cannot write its product before the timeout, the TIMEDOUT exception is raised. Producer will get off the mel store writers' queue, and processes the <resume> operation to try to deposit again.
    If we were using continue instead of <resume>, we would loop back the while loop, which would cause a new product to be Produced. The product that got timed out would be lost. If the logic of the program were to skip any timed out item (perhaps because it had become obsolete), then continue would be the right choice.
  8. If Consumer has processed 500 items, it will close the mel element store. On the next iteration of the while loop, the Producer will get hit with the CLOSED exception, when it tries to write to the mel store.
     
  9. When the Producer receives the CLOSED exception, it will get off the mel store writers' queue, and processes the CLOSED catch handler. Here, it breaks out of the while loop, and with nothing else to do, it will end itself.
If during an attempt to write to the mel store, the Producer detects an exception other than TIMEDOUT or CLOSED, it will get off the mel writers' queue, and jumps to the catch-all handler:
catch ( store<...> )
Here, it prints the Unexpected exception statement with the error code and cause of the exception, and then breaks off the while loop. Without the catch-all handler for store, any unexpected and thus unprocessed exception from store will cause the Producer task to abort abruptly, instead of ending graciously.

Mel Write Conditions

As a summary, the following conditions must be met before a task can write in a mel value:
  1. The task is at the top position of a writers' queue
  2. The writers' queue has been granted WRITE access to the mel element
  3. The mel element must have a vacant slot for the task to deposit its product
The first condition is relevant when there are other tasks already in the queue. Only the task at the top of the queue can deposit its product to the next available slot of the mel buffer from the writer's end.

The second condition is relevant when there are multiple queues vying for access to the same mel element. A queue with a higher priority is granted access to the mel element more often than lower priority queues.

The third condition is relevant when the mel element is full. The writer task must wait for a reader task to remove a value in the mel buffer to make room for the deposit.

Mel Write Buffers

The purpose of a mel write request is to deposit a new value to the mel element. If the mel element is not buffered -- which is the default for a mel declaration, the writer task has to wait for a reader task to empty the mel element to make room. If the mel element is buffered, the writer task can deposit into an empty slot in the buffer until the buffer is full.

The availability of a mel buffer differentiates a mel write from a mel read. A read can only work with only the top value of a mel. This also how a write works if the mel is not buffered. If a programmer buffers a mel variable, a writer can deposit in the empty slots of the buffer without waiting for a reader to empty the top slot. With enough resources assigned to the buffer, a mel write wait can be greatly minimized.
Note that we have been using the term mel buffer even on mel elements that are not buffered. In these cases the mel buffer is considered to be of size one (i.e. having one slot).


Mel Writeover Mode

In addition to the default writer mode which requires the task to wait for a vacant mel buffer, the mel write operation supports the writeover mode that does not require such wait. This mode behaves like the writer mode if the mel buffer is vacant, but if the buffer is full or filled, it will overwrite the last written item without looking for an empty slot.

Let's create a new Producer that creates a "good-enough" product first for impatient consumers. It then produces a "better" product to replace the "good-enough" product if the latter has not been retrieved by any consumer.
void Producer (<mel> int store) {
  int c;
  while ( 1 ) {
    if ( c = ProduceGood() ) break;
    <?>store = c;
    if ( c = ProduceBetter() ) break;
    <? mode=writeover>store = c;
  }
  <close>store;
}
The mode keyword is optional. The Producer could also make this statement:
<?writeover>store = c;

The above Producer makes two write operations to the mel variable store. The first one is a normal write operation using the default writer mode where it has to wait for a vacant slot in the mel buffer. The second write overwrites the value from the first write if it has not been picked up by a consumer; otherwise it just deposits the "better" product for the next consumer.

This is the behind-the-scene processing of a generic task doing a writeover to a mel element. The first steps are the same as those for a writer mode:
  1. The task generates a new product and puts itself into one of many possible writers' queues of the mel element.
     
  2. The writers' queue the task selects to stand in line is based on the priority of the mel write operation. If no priority attribute is explicit, the task joins the default writers' queue.
     
  3. If there are other writer tasks in the queue, the task stays in the queue, and moves up to the front of the queue as the tasks in front of it get off the queue.
     
  4. At the front position, the task waits for the queue to have WRITE access to the mel buffer due to competition from higher priority queues, if any.
     
  5. After its queue is granted WRITE access, the task checks for the status of the mel buffer. This is where the writeovr mode can differ from the default writer mode:
    • If the mel buffer is empty (i.e., all mel buffer slots are vacant), the task deposits its product at the writer's end of the mel buffer. This is the same as in a writer mode.
       
    • If the mel buffer filled (i.e., some slots are vacant and some not), the task overwrites the last written slot with its own product. This differs from the writer mode where the task deposits its product on an available vacant slot.
       
    • If the mel buffer is full (i.e., none of the slots are vacant), the task overwrites the last written slot with its own product. This differs from the writer mode where the task waits for a vacant slot to be made available.
  6. The task increases the sequence number of the deposited or overwritten item, and gets off the mel queue with no error reported.
     
  7. If there is an error during the writeover process, the task will get off the mel queue without making any new deposit or overwrite in the mel buffer, and raises an exception.
The above Producer attempts to overwrite a "good-enough" product with a "better" product, but a fast Consumer task can still retrieve the "good-enough" product before Producer has time to overwrite it. In addition, if there is another writer task that inserts its own product right after Producer writes in its "good-enough" product, the value that Producer overwrites with its "better" product will be of the other writer task since this value is the last written value.

There are several uses of a writeover mode in NERWous C:
  1. Ad-hoc use like in the above example, to fit the logical need of a program
  2. As the write mode for a read-only mel
  3. As the write mode for a release statement
  4. As the write mode for exclusive zones


Mel OR Writes

Like the mel OR read, there is a corresponding mel OR write. Let's consider a Producer task that can send its product to either store1 or store2 mel elements, depending on which one is available to receive the new value. There are two methods that such a task can use to traverse the mel elements:

Mel OR Write Methods
Method ValueDescription
SerialThe task checks the mel elements serially based on the order in the OR list.
RandomThis is the default method when no method is specified. A random generator dictates which mel element in the OR list is checked first. The task then circulates through the remaining mel elements in the list. This method is more compute intensive but gives a fairer chance to all mel elements in the OR list.


Serial OR Writes

Let's start with a Producer that uses the Serial method:
void Producer (<mel> int store1, <mel> int store2) {
  while ( <? method=serial>(store1 || store2) = Produce() );
  <close>store1;
  <close>store2;
}
The keyword method can be omitted:
while ( <? serial>(store1 || store2) = Produce() );
This is the behind-the-scene process of the Serial mel OR write statement:
<? serial>(store1 || store2) = Produce()
  1. The Producer task Produces a product, and puts itself onto store1 default writers' queue.
     
  2. If the mel WRITE conditions are satisfied on store1, Producer will deposit its product into store1 buffer at the writer's end, increases the sequence number on store1, gets off the mel store1 default writers' queue, and exits the mel OR write statement. The store1 mel element is now filled for a reader task.
     
  3. If the mel WRITE conditions are not met at store1, the Producer continues to wait in the store1 writers' queue, and also puts itself to store2 default writers' queue.
     
  4. If the mel WRITE conditions are satisfied on store2, Producer will deposit its product into store2 buffer at the writer's end, increases the sequence number on store2, removes itself from both mel writers' queues, and gets off the mel OR write statement. The store2 mel element is now filled for a reader task.
     
  5. Otherwise, Producer will wait on both queues, until the mel WRITE conditions become true for either one. The Producer then deposits its product to the identified mel buffer, increases the sequence number on that mel element, removes itself from both queues, and gets off the mel OR write statement. Either store1 or store2 has been filled, but Producer does not know which one.
     
  6. If it happens that the mel WRITE conditions become true at both mel elements at the same time, the CHAOS runtime will ensure that Producer will write to one or the other mel element , and not both.
In the above example, Producer keeps producing and depositing its product to either store1 or store2 whichever is vacant at that time, until it Produces a zero value. It then breaks off the while loop and closes both mel channels.

Random OR Writes

Although either mel element can be selected, there is a bias for the first mel element in the Serial OR write method, due to it being looked on first when the OR write starts. If this first mel element is always vacant, perhaps due to it having an ample buffer or working with a fast reader task, it will keep being picked up, shunting the rest from ever be. If the logic of the program demands that all mel elements in the mel OR write have a fair chance, then the Random method is used:
void Producer (<mel> int store1, <mel> int store2) {
  while ( <? method=random>(store1 || store2) = Produce() );
  <close>store1;
  <close>store2;
}
The keyword method can be omitted:
<? random>(store1 || store2) = Produce()
Since the Random method is the default method for mel OR writes, the keyword value random can also be omitted:
<?>(store1 || store2) = Produce()
The behind-the-scene process for the Random mel OR write starts with the task picking a mel element in the mel OR list in a "fairly random" manner. NERWous C does not define a particular strategy for "fairly random" as long all the mel elements in the mel OR list has a fair chance to be picked first.

Once the starting element is picked, the Random method follows the Serial method. After the last mel element in the OR list is checked and found not available for writing, the Random method wraps around and checks the first element in the mel OR write list.

OR Write Exceptions

The above Producer assumes no error to happen in the mel OR write operation. The following Producer is hardened with exceptions to handle possible errors:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      int c = Produce();
      if ( !c ) break;
      <? timeout=100>(store1 || store2) = c );
    }
    catch ( (store1 || store2)<TIMEDOUT as=store> ) {
      printf ("The mel element [%s] has timed out", store<name>);
      <resume>;
    }
    catch ( (store1 && store2)<CLOSED> ) {
      printf ("Both mel elements have closed");
      break;
    }
    catch ( (store1 || store2)<... as=store> ) {
      printf ("The mel element [%s] has error [%d]", store<name>, store<error>);
      if ( store<error> == NERW_ERROR_CLOSED ) <resume>;
      break;
    }

    <close>store1;
    <close>store2;
}
The exception rules for mel OR writes are similar to the mel OR reads rules:
  • Skipping rule: if an exception happens to a mel element, and there is no specific handler for that mel exception, the CHAOS runtime will remove the task from waiting on that mel element. The mel OR write continues with the mel elements that have not had any exception.
     
  • Aborting rule: if there is a specific handler for that mel exception, the mel OR write is aborted. The task gets off all the waiting mel queues, including those that belong to mel elements that do not have exceptions; and the task will process the identified handler.
     
  • Precedence rule: individual exception handlers take precedence over combination exception handlers. An AND combination exception handler takes precedence over an OR combination exception handler.
Based on the above rules, we can simplify Producer as follows:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      <? timeout=100>(store1 || store2) = Produce() );
    }
    catch ( (store1 && store2)<...> ) {
      if ( store1<error> == NERW_ERROR_TIMEDOUT || store2<error> == NERW_ERROR_TIMEDOUT)
          <resume>;
      break;
    }
  }
  <close>store1;
  <close>store2;
}
In this new Producer, if one mel element gets into an error, it will be automatically skipped; and the mel OR write continues with the other mel element. There is no need for an explicit handler for this built-in behavior.

If both mel elements get any kind of error (same or different errors), then the task gets off the mel OR write, and processes the AND combination catch-all exception handler. If one or both errors are a TIMEDOUT, the task does a resumption in order to try to deposit the current product from Produce again. The timed-out mel element or elements will be cleared on this error on exit of the handler, and can accept a write deposit. (If the logic of the program says to discard timed-out products because they are deemed "stale", the continue statement should be used to re-iterate the while loop for a new product, instead of the <resume> statement.)

If neither mel elements get a TIMEDOUT error (i.e. both get a CLOSED or some other error), the task breaks off the infinite while loop. The <close> operations that Producer does on the mel elements indicate to the other tasks in the program that Producer is done with the mel elements. These operations are extraneous if the mel elemnents are already closed by other tasks.

Mel AND Writes

While the mel OR writes allow writing into one of the listed mel elements, the mel AND writes are for writing to all of them. The AND mel write has several built-in methods to process the listed mel elements:
Mel AND Write Methods
Method ValueDescription
SerialThe task writes to the mel elements serially based on the order in the AND list.
ParallelThis is the default method when no method is specified. The task makes a parallel attempt to write to all the mel elements in the AND list. The writes occur individually whenever a mel element is available for writing by this task.
InSimulThe task makes a parallel attempt to write to all the mel elements in the AND list. The writes occur simultaneously when all mel elements are available for writing by this task.


Serial AND Writes

To illustrate the mel AND write Serial method, let's first replace the Produce function with ProduceTwo which returns two values:
void Producer (<mel> int store1, <mel> int store2) {
  int c1, c2;
  while ( 1 ) {
    (c1, c2) = ProduceTwo();
    <? timeout> store1 = c1;
    <? timeout> store2 = c2;

    if ( !c1 || !c2 ) break;
  }
  <close>store1;
  <close>store2;
}
Once ProduceTwo generates the two products, they are stored in the local variables c1 and c2. The Producer task then writes the first product to the mel buffer store1. Once that successful, it writes the second product to the mel buffer store2. After both writes are done, Producer checks if any product is 0 which marks the end of the production; in that case, it breaks out of the while loop and lt;close> the mel buffers to signal to any reader tasks the end of production.

The above Producer code is now simplified with the mel AND write statement:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      <? timeout serial>(store1 && store2) = ProduceTwo() );

      if ( !store1<value> || !store2<value> ) break;
    }
    catch (store1 || store2)<TIMEDOUT as=store>) {
      printf ("Can't put into %s in time", store<name>);
      <resume> store;
    }
    catch (store1 || store2)<CLOSED as=store>) {
      printf ("%s has closed", store<name>);
      break;
    }
  }
  <close>store1;
  <close>store2;
}
The optional keyword method can be used if one desires to be explicit:
<? timeout method=serial>(store1 && store2) = ProduceTwo() );
Let's look at the behind-the-scene process of the Serial method for a mel AND write:
  1. The Producer task runs ProduceTwo and generates two new items.
     
  2. The Producer task puts itself to store1 default writers' queue with the default timeout value.
     
  3. If the Producer cannot put its item into store1 buffer before the timeout, it aborts the mel wait on store1, exits the mel AND write, and triggers the TIMEDOUT exception on store1.

    The handler
    catch ( (store1 || store2)<TIMEDOUT as=store> )
    will pick up this exception. It printfs out a message then invokes <resume> to put itself back at the bottom of the store1 writers' queue to try to deposit the first produced item again.
     
  4. If the mel WRITE conditions are met at store1 before the timeout, the Producer deposits the first produced item into the mel buffer, and drops out from that mel writers' queue. This allows other writer tasks to write to the mel element store1.
     
  5. The Producer task now puts itself to store2 default writers' queue with the default timeout value.
     
  6. If the Producer cannot put the second produced item into store2 buffer before the timeout, it aborts the mel wait on store2, exits the mel AND wait, and triggers the TIMEDOUT exception on store2.

    The handler
    catch ( (store1 || store2)<TIMEDOUT as=store> )
    picks up this exception. It printfs out a message then invokes <resume> operation. This operation knows that the first produced item has been successfully deposited in store1. It therefore puts the Producer task back to the store2 writers' queue (at the bottom position) to try to deposit the second produced item again.
     
  7. If the mel WRITE conditions are met at store2 before the timeout, the Producer deposits the second produced item into the store2 mel buffer, and drops out from that mel writers' queue. This allows other writer tasks to write to the mel element store2.
     
  8. The Producer task exits the mel AND write statement with both produced items deposited into the corresponding mel elements, one after the other as dictated by the Serial AND write method.
     
  9. If the Producer task encounters a mel closure from any mel element during the mel waits, the CLOSED exception will be raised, and handled by:
    catch ( (store1 || store2)<CLOSED as=store> )
    This exception handler does a printf on the closing mel element and breaks out of the while loop.

    What would happen if a <resume> or continue were used instead of the break? The <resume> operation would clear all exceptions, including the CLOSED exceptions, retry the mel AND write on produced item that have not been written by sending a request to the remote mel elemnt(s), get back the piggy-backed status that the mel elements have been closed, jump into the CLOSED exception handler again, and keep repeating the same process, causing the Producer task to livelock -- i.e. to spin endlessly on the same produced items.

    The continue would also cause a livelock, but in a different way. This statement would not clear the exceptions, then cause the Producer to loop back to the while loop, ProduceTwo two new items, attempt to write them to the closed mel elements, detect locally that the mel elements already have the CLOSED status, replace the sending a write request to the remote mel elements with the jump into the CLOSED exception handler. This spinning is then repeated endlessly, each time producing and discarding two new products.
In summary, the Serial method for mel AND writes is just a more compact way to write the one-at-a-time program.


Parallel AND Writes

Let's now change Producer again so that it still add the products of ProduceTwo to both mel buffers, but we will remove the requirement that the additions to be done at the same time. Instead, they will be added at the same time. This AND write method is therefore referred to as the Parallel method:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      <?>(store1 && store2) = ProduceTwo() );

      if ( !store1<value> || !store2<value> ) break;
    }
    catch (store1 || store2)<TIMEDOUT as=store>) {
      printf ("Can't put into %s in time", store<name>);
      <resume> store;
    }
    catch (store1 || store2)<CLOSED as=store>) {
      printf ("%s has closed", store<name>);
      break;
    }
  }
  <close>store1;
  <close>store2;
}
The Parallel method is the default method for the mel AND write, and is assumed when no method is requested, as in the example above. A programmer can choose to be explicit:
<? timeout method=parallel>(store1 && store2) = ProduceTwo() );
Like always, the keyword method can be omitted:
<? timeout parallel>(store1 && store2) = ProduceTwo() );
This is the behind-the-scene processing for a mel AND write with the Parallel method:
  1. The Producer task runs ProduceTwo and generates two new items.
     
  2. The Producer task puts itself to both store1 and store2 writers' queues.
     
  3. Whenever the mel WRITE conditions are satisfied for one mel element, Producer will deposit the corresponding product into that mel buffer, and removes itself from that mel writers' queue.
     
  4. Whenever the mel WRITE conditions are satisfied on the remaining mel element, Producer will do the same thing as above for this mel element.
     
  5. If the mel WRITE conditions are not satisfied in time for one of the mel queues, the Producer task will get off the mel queue, and jumps into the handler
    catch ( (store1 || store2)<TIMEDOUT as=store> )
    It printfs out a message then invokes <resume> on that mel element. This operation will clear the exception on that mel element, puts the Producer task back at the bottom of that writers' queue to attempt to deposit the correspondent product again.
     
  6. Once the Producer task has both products successfully written into their corresponding mel buffers, it gets off the mel AND write statement.
     
  7. If while waiting in one mel element queue, the Producer task may get timed out in one (or both)
The randomness with the mel LIST write is that we don't know which mel buffer is written to first. If the order is important, then the Producer code must be explicit:
void Producer (<mel> int store1, <mel> int store2) {
     int c1, c2;
     while ( 1 ) {
          if ( (c1, c2) = ProduceTwo() ) break;
          /* Write to store1 then store2 */
          <?>store1 = c1;
          <?>store2 = c2;
     }
     <close>store1;
     <close>store2;
}



InSimul AND Writes

A mel AND write is used when a programmer wants both store1 and store2 to be written to at the same time. Let's now change Producer so that it calls a function ProduceTwo which returns two values. These values will be respectively inserted to store1 and store2 mel buffers at the same time.
void Producer (<mel> int store1, <mel> int store2) {
     while ( <?>(store1 && store2) = ProduceTwo() );
     <close>store1;
     <close>store2;
}
For example, we can think of a gaming program where a launching of nuclear missile requires a squeeze on a trigger and a press of a button to be received simultaneously. Of course for this program to work, the mel variables that represent the trigger and the button ought to be of buffer size 1, since it would be meaningless to buffer such requests.

The mel AND write operation behaves similarly to a mel AND read to prevent deadlocks in case of multiple tasks accessing the same mels for writing. The difference is that instead of waiting for a mel to be valued, a writer task waits for a mel to have an available slot at the mel buffer writer's end so it can deposit its product into. If the buffer is of size greater than 1, it is easier to satisfy a mel AND write than a mel AND read.

This is the behind-the-scene processing for a mel AND write:
  1. The task puts itself in both mel writer's queues.
     
  2. In due course, the task will be at the first position of one the mel writers' queues and joins the "top-of-the-queue" writers group of that queue. It then checks if it is also in the "top-of-the-queue" group at the other mel writer's queue. If it is not, it will wait for it to join the "top-of-the-queue" group at the other writers' queue.
     
  3. While waiting in only one of the "top-of-the-queue" group, if another writer task comes along on that writers' queue, the task will allow that later-coming task to "jump the line" and deposit its value to an available empty buffer slot if there is one.
     
  4. Eventually the task will be in the "top-of-the-queue" groups at both mel writers' queues. It will keep checking for empty slots at both mel buffers. If none is available, it will wait. If only one buffer is available, it will wait, but this time, it will not allow any later-coming writer task, that is not in the "top-of-the-queue" group, to "jump the line" and fill that available buffer slot.
     
  5. Eventually the task will find empty slots at both mel buffers. It will then fill the mel buffer slots with its products, increases the sequence number of both mel variables, removes itself from both writers' queues, and gets off the mel AND write.

There can be more than one task in a "top-of-the-queue" group. This group is reserved for tasks that are doing AND writes. When a task doing an AND write moves up to the first position in a writers' queue, it may not find all the conditions for a successful AND write at that moment. Instead of relinquishing the first position in the queue, it joins the "top-of-the-queue" group, and bides its wait there.

For example, we have a task T1 that does an AND write on mel resource R1 and R2. T1 is in the "top-of-the-queue" group on R2 but not yet on R1. Then a task T2 comes along and does a normal write on R2. If there is mel buffer slot available, T1 will allow T2 to "jump the line" and fills that slot. Eventually, T1 is in the "top-of-the-queue" groups of both R1 and R2. However one or both mel buffers are still full, so T1 waits. When the cyclic T2 comes back for another product deposit run, it cannot "jump the line" this time. It has to wait. The next available empty slot is already reserved to T1 since T1 has achieved the "top-of-the-queue" condition at both queues, and now needs the "available mel buffer slot" condition in order to fulfill its AND write.

Let's keep the above example but add a task T3 that does an AND write on resource R2 and R3. When T3 gets to the first position on R2 but not on R3, it will join T1 in the "top-of-the-queue" group of R2. Now if R2 becomes available, the CHAOS runtime automatically assigns it to T1 and not T3 since the former has attained "top-of-the-queue" in all the mels in the AND list, and not the latter.

Eventually T3 also joins the "top-of-the-queue" group of R3, and has achieved "top-of-the-queue" in all the mels in its AND list. Now if R2 becomes available, CHAOS will also give that slot to T1 because it has joined the "top-of-the-queue" group of R2 before T3. T1 now can satisfy all its AND write conditions, deposits its products and gets off both writers' queues. T3 has to wait for the next empty slot from R2 to go with the R3 slot it already has, before it can exit its AND write. The task T2 that is waiting for both T1 and T3 to be done with R2, can now access the next empty slot from R2.


Previous Next Top

Wednesday, January 17, 2018

A Tour Of Go Language

Welcome » NERWous C » Examples
  1. Go Concurrency
  2. Fibonacci Example
  3. Mutex Example


Go Concurrency

The Go language is an open-source programming language created by Google in 2009. It supports concurrency via Goroutines and channels. The NERWous C equivalents are pel constructs, and mel entities.


Fibonacci Example

The Fibonacci sequence appears frequently in mathematics, computer algorithms, and biology settings.

Go Implementation:

The Go language Fibonacci example displays the first 10 Fibonacci numbers (i.e. 0, 1, 1, 2, 3, 5, 8, 13, 21,34). This implementation is copied from the Tour Of Go tutorial:
package main
import (
 "fmt"
)
func fibonacci(n int, c chan int) {
 x, y := 0, 1
 for i := 0; i < n; i++ {
  c <- x
  x, y = y, x+y
 }
 close(c)
}

func main() {
 c := make(chan int, 10)
 go fibonacci(cap(c), c)
 for i := range c {
  fmt.Println(i)
 }
}
The Go example has main creating the channel c then forking a light-threaded task via the go command to run the fibonacci function in concurrency. The fibonacci task feeds the channel c whenever it has computed the next number for the sequence. The main task waits on the channel c for any new value to show it on the console via the Println command.

NERWous C Implementations:

We will do several versions of the NERWous C implementations to illustrate different features of the language.

Version 1:
Version 1 follows closely the Go implementation by replacing the Goroutine and Go channel with the pel and mel constructs of NERWous C:
/* VERSION 1 */
void main () {
  <mel> int c;
  <!> fibonacci (10, c);
  for (int i=0; i<10; ++i)
    printf ("%d", <?>c);
}
int fibonacci (int n, mel int c) {
  int x = 0, y = 1;
  for (int i = 0; i < n; ++i ) {
    <?>c = x;
    x = y; y = x + y;
  }
  <close>c;
}
The mel variable c is shared between the main task and the fibonacci task. The main task is the reader and the fibonacci the writer. When main tries to read out from the mel c and the fibonacci has not generated a value for it, main will wait at the mel wait operator <?>. When the mel channel c is filled, main reads out that value and makes the mel c empty again.

The fibonacci task needs the mel channel c to be empty so that it can deposit the x value. If the reader main is slow in removing the previous value, the fibonacci writer has to wait at the mel wait operator <?>.

Although the main and fibonacci tasks run in concurrency, they work in lock step due to the synchronizing chock point at the mel channel c. To possibly increase the concurrency, we can add buffering to the mel c:
<mel buffer=10> int c;

Version 2:
In the second version, we will drop the mel variable c and use the return value of the pel task as the communication channel.
/* VERSION 2 */
void main () {
  <pel> p = <!>fibonacci (10);
  for (int i=0; i<10; ++i)
    printf ("%d", <?>p);
}
int fibonacci (int n) {
  int x = 0, y = 1;
  for (int i = 0; i < n; ++i ) {
    <release> x;
    x = y; y = x + y;
  }
}
The return value of a pel task is a read-only mel. Via the <release> statement, the pelled task "streams" values to any task that is waiting on that return value channel. Unlike Version 1 where the main task depends on the mel entity to be filled, the main task in Version 2 uses the sequence number to make sure it does not process stale values.

Version 3:
The third version builds on the second version by having the main task keeps checking the released value of the fibonacci task until this task ends itself.
/* VERSION 3 */
void main () {
  <pel> p = <!> fibonacci (10);
  while ( 1 ) {
    try { printf ("%d", <?>p); }
    catch (p<ENDED>) break;
  }
}
int fibonacci (int n) {
  int x = 0, y = 1;
  for (int i = 0; i < n; ++i ) {
    <release> x;
    x = y; y = x + y;
  }
}
When the fibonacci task runs off its last statement, it will end itself. This triggers the ENDED exception at the main task which uses it to break out of the infinite while ( 1 ) loop.


Mutex Example

The Go language support the concept of a mutex for mutual exclusive access to shared data. A Go language block of code can be executed in mutual exclusion by being surrounding with a Lock and Unlock calls.

Go Implementation:

The Go implementation is taken from Tour of Go tutorial:
package main

import (
 "fmt"
 "sync"
 "time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
 v   map[string]int
 mux sync.Mutex
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
 c.mux.Lock()
 // Lock so only one goroutine at a time can access the map c.v.
 c.v[key]++
 c.mux.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
 c.mux.Lock()
 // Lock so only one goroutine at a time can access the map c.v.
 defer c.mux.Unlock()
 return c.v[key]
}

func main() {
 c := SafeCounter{v: make(map[string]int)}
 for i := 0; i < 1000; i++ {
  go c.Inc("somekey")
 }

 time.Sleep(time.Second)
 fmt.Println(c.Value("somekey"))
}
NERWous C Implementations:

NERWous C is the NERW Concurrency Model grafted on the C language, and the C language does not have native support for map nor string. The following NERWous C language implementations make use of a simple array with integer as index. This will not diminish the discussion on Mutex which is the focus here.

Version 1:
/* VERSION 1 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void Inc (int somekey) {
   <?>SafeCounter[somekey] {
      SafeCounter[somekey]++;
      <checkout replace>;
   }
}

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i) {
      <!>Inc (somekey);
   }
   sleep (1000);
   printf ("%d", <?>SafeCounter[somekey]);
}
In the first version, we first declare SafeCounter as a global mel array that can contain NUMKEYS items. The value of NUMKEYS come from the include file keys.h.

The main task runs a serial loop to initialize to 0 each of the item of SafeCounter. It then generates a random number between 0 inclusive and NUMKEYS exclusive via a custom function random (which we do not show the implementation). The main task then runs a second serial loop for 1000 iterations. In each iteration, it forks a new task via the pel construct to run the Inc function in parallel.

Inside the Inc function, the deceptively simple auto-increment means a read of the value of the mel entity SafeCounter[somekey], increment that value, and write that value back to the mel entity. Since the read and write must be atomic, all the mentioned operations must be inside a mel reader zone for mutual exclusion. When an Inc task is in the exclusive zone, all the other Inc tasks that needs access to the same SafeCounter[somekey] entity, must queue up in that entity's queue.

The first Inc task that comes to the reader zone can see that the conditions to get into the zone are satisfied. These conditions are the mel entity is valued and that value is not stale. These conditions are made satisfactory by the main task when it initializes all the items in the SafeCounter array to zero.

After doing the auto-increment using the local eponymous variable SafeCount[somekey], the Inc task writes back that value to the shared mel entity via the <checkout replace> operation. Without the replace attribute, the default checkout behavior is to remove the value from the mel entity. This would cause all the waiting Inc tasks to wait forever since there is no new writer task to deposit a fresh value to the mel entity to satisfy the reader zone entry condition.

Back to the main task, we see that it wants to display the final value of SafeCount[somekey]. Since it does not know when all the pelled Inc tasks will all finish, it sleeps for 1 empirical second. To access the mel value at that time, the main task just use the mel read statement <?>SafeCounter[somekey]. There is no need for using a mel read exclusive zone since a NERWous C mel read natively enforces mutual exclusion.

Version 2:
Let's revise the above implementation. Since NERWous C supports inline code blocks, we can drop the Inc function and move its work inside the main task:
/* VERSION 2 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i) {
      <!> <?>SafeCounter[somekey] {
      SafeCounter[somekey]++;
      <checkout replace>;
      }
   }
   sleep (1000);
   printf ("%d", <?>SafeCounter[somekey]);
}
Version 3:
Since auto-increment (and decrement) are frequently used operations, NERWous C supports shortcuts that skip mentioning the explicit use of the mel read zone:
/* VERSION 3 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i)
      <!> <?>SafeCounter[somekey]++;

   sleep (1000);
   printf ("%d", <?>SafeCounter[somekey]);
}
Version 2 and 3 are the same. In both Versions, the mel entity is replaced in place, thus preserving its sequence number. The next task that accesses the mel entity will see a new value but the same sequence number. Sequence numbers are used to prevent re-processing of stale values. This is not an issue with the Mutex example here, since each pelled task that does the auto-increment goes through the SafeCounter[somekey] mel entity once and does not do loops. Loops are where stale values can be encountered.

Version 4:
In the previous versions, we have a sleep operation because we want to display the final value of SafeCounter[somekey], but since the auto-increment tasks run in parallel, we don't know when they all finish. The one second is an empirical value that seems to work in the running environment. (If it does not, just increment it until it does.) NERWous C supports a pel-for loop that allows us to remove the sleep operation:
/* VERSION 4 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   <begin> { for (int i=0; i<1000; ++i)
      <!> <?>SafeCounter[somekey]++;
   } <? ENDED>;

   printf ("%d", <?>SafeCounter[somekey]);
}
Encapsulating the for loop inside the begin-wait-ended block ensures that all pelled tasks in the for loop have ended before the programming flow continues. When main issues the printf command in Version 4, it is assured that all auto-increment tasks are done and the mel entity SafeCounter[somekey] contains the final value.

Version 5 (Serial):
There is no NERWous C Version 5. Version 4 is as compact as it can be. However notice that if we strip out the NERWous C pel and mel constructs, we end up with the correspondent C program:
/* VERSION 5(Serial)  */
#include "keys.h"
int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i)
      SafeCounter[somekey]++;

   printf ("%d", SafeCounter[somekey]);
}
Sometimes, with NERWous C, by sprinkling some <?> and <!> here and there, we transform a serial program into a concurrent program without much ado.


Previous Next Top