Pages

Sunday, September 10, 2017

Mel Properties

Welcome » NERWous C » Mel
  1. Mel Properties Cache
  2. Mel Properties Categories
    1. IN Properties
    2. OUT Properties
    3. SET Properties
    4. CORE Properties
  3. Mel Properties Constants
  4. Mel ID Property
  5. Mel URL Property
  6. Mel Value Property
  7. Mel Value Auto Operators
  8. Mel Status Property
  9. Mel Condition Property
  10. Mel Name Property
    1. Mel Name Scope Rule
    2. Mel Element Reference
    3. Mel Name Usage


Mel Properties Cache

The NERW Concurrency Model posits that tasks run in processing elements (pel) and share data contained in memory elements (mel). The pels and mels are hosted on computer elements (cel). The cels are distributed over a network of execution, read and write (nerw). When accessing shared data, a pel may have to cross the nerw network to reach a different cel that hosts the requested mel. This effort is resource intensive and incurs latency, reducing the throughput of a concurrent program.

To minimize the nerw impediments, NERWous C supports the concept of properties. Whenever a task takes on an operation that reaches out to a mel, the data that is returned to the task not only contains the result of the operation, but also other information about the mel. This information is saved locally into a cache set up inside the mel variable of the same name as the remote mel element. This eponymous mel variable represents the connection from the task to the mel element. Pieces of information in that cache are called properties.

The properties cache is the view of the mel at the time of the last operation the task conducts against that mel. This view will eventually become stale when other tasks update the shared mel element. To refresh its properties cache, a task can do another altering mel read or write operation, or invoke the non-altering snapshot operation.


Mel Properties Categories

This section expands on the introduction of mel properties categories from the last chapter. As discussed, there are three types of properties:
  1. IN properties are cached attributes from the last mel operation a task conducts against a mel
  2. OUT properties are results the task caches after receiving them from a mel operation
  3. SET properties are configuration information from the mel that are piggy-backed with each mel operation
  4. CORE properties are system values assigned by the CHAOS runtime to allow a task to reach out to a mel
Let's explore these properties, using the mel variable store as a stand-in.

IN Properties

The IN properties pertain solely to the requesting task. Unlike the OUT, SET and CORE properties, the IN properties do not belong to the mel element. They are attributes to the mel operation that the task initiates, and are specific to that mel operation. Another task may use different attributes in its own mel operation, and will have different IN properties values in its resident mel variable. These operational attributes are saved as properties so the program can refer back at them, for reporting, debugging, or subsequent invocations. As an example of the latter case, a task can adjust the priority of the next mel request based on the length of the wait from the previous request to average out the latency.

PropertySynopsis
store<priority>Priority value used by the last mel operation
store<timeout>Timeout value used by the last mel operation
store<name>Scoped name of the mel variable set during creation
store<agent>Entity that executes a requested mel operation

When a task does a new mel operation to the mel element, the IN properties in the mel variable are cleared, and re-initialized with the attributes of the new mel operation.

OUT Properties

The OUT properties cache the result returned by a mel operation.

PropertySynopsis
store<value>Value returned by the mel read operation. This value can be a simple entity (like an int), an array or a structured entity
store<count>Number of items in an array value
store<sequence>Version identifier of the value returned from a mel read operation
store<status>Status of the mel variable after the mel operation
store<condition>Status of the mel variable before the mel operation
store<error>A numerical value representing the error code of the last mel operation. A value of 0 means success and non-0 values mean failures.
store<why>Reason of a non-zero <error> value

The OUT properties reflect information about the mel element after the mel operation has completed for the task, either successfully or unsuccessfully. After awhile, this information becomes stale due to other tasks doing reads and writes on the mel element. To get the current information, the task must invoke a new read or write operation, or take a new snapshot of the OUT data via the snapshot operation.

SET Properties

The SET properties are additional information about the mel element that are piggy-backed on return of any mel operation. These properties are snapshots of the configuration setting of the mel element at the time of return. After awhile, these properties may be stale if other tasks have issued mel operations that change the configuration, such as the rebuffer operation. If up-to-date values are needed, the program should make another mel operation or use the snapshot operation to refresh the SET properties.

PropertySynopsis
store<buffer>Number of buffer slots of the mel element
store<location>Cel location of the mel element
store<readers>Number of reader tasks subscribing to the mel element
store<writers>Number of writer tasks subscribing to the mel element

The SET properties are system properties that can be changed programmatically by using mel operations. The CORE properties that we will explore next are system properties that cannot be changed programmatically once they are assigned.

CORE Properties

The CORE properties are system values assigned by the CHAOS runtime to allow a task to reach out to a mel element. These values cannot be changed once a mel element has been created.

PropertySynopsis
store<id>Mel variable numerical identification. This value is mostly used in environments where shared resources can be identified by a number, such as multi-CPUs shared-memory supercomputers. Otherwise this value is a perfect hash of the <url> property.
store<url>Mel element string identifier. This value is mostly used in distributed environments where shared resources are identified by a uniform resource locator. Otherwise this value is just a string representation of the numerical <id> property.

The CORE properties are identification properties. They are the same and remain constant in all the mel variables of all the tasks that refer to the same mel element.


Mel Properties Constants

Some mel properties have a wide range of application-specific values. Other mel properties, such as error and status have a fixed set of constant values. In NERWous C, these constant values are defined in the nerw.h file that must be included in all NERWous C programs.

For brevity, the inclusion of nerw.h is mostly skipped in the code samples.


Mel ID Property

The <id> property is assigned by the CHAOS runtime environment to uniquely identify a mel element. The type of this property is a long long, allowing a theoretical limit of over 9 quintillion mel elements (i.e. 9 followed by 18 zeros). Although NERWous C does not specify how an <id> property is formatted, it requires that:
  1. The <id> property is assigned to a mel element when it is created
  2. No two mel elements within the same NERWous C program can have the same <id> property
The <id> property can be used for tracking or debugging:
<mel> int store;    /* the <id> property is set */
printf ("Mel entity [%ll] created by declaration", store<id>);
Another use is to differentiate between the selected mel elements in a reader OR zone.

When a mel variable is declared, the associated mel element is also created. At this time the CHAOS runtime will assign a unique identification number to represent the mel element, and stores this value in the <id> property of the mel variable. If the task passes the mel variable to another task, the <id> property is passed verbatim. This allows both tasks to refer to the same mel element.

The <id> property is maintained by the CHAOS runtime, and cannot be assigned from a NERWous C program. Attempting to do so will trigger a compile-time error:
store<id> = 0LL;    /* Compile-time ERROR */
In a distributed environment where a resource can be better identified via a universal resource locator (URL) string, the <id> can be the numerical perfect hash of the <url> property. The NERWous C language does not dictate a perfect hash algorithm, but with 9 quintillion numerical possibilities, it should be possible to hash structured <url> strings into unique numbers.


Mel URL Property

The <url> property is the string equivalent to the <id> property. It is assigned by the CHAOS runtime environment to uniquely identify a mel element. The equivalence between the <id> and <url> means that either of them can be used for identification. Which format to use depends on the actual implementation of the mel operation and most importantly to the physical nature of the mel element.

If in use, a string identifier is not any random string value, but structured in certain way to fit the underlying physical environment. For example, if a mel element is a shared file, then a <url> value will use a file structure, such as /share/mel/store.dat. If it is hosted on a web site, a <url> value for a mel element of name store can be something like https://nerw.dom/mel/store, and mel operations such as this creation mel statement:
<mel buffer=10> int store
can be translated into this URL
https://nerw.dom/mel/store?action=create&buffer=10
In environments where a numerical identifier is more compatible, the <url> string identifier is its alphanumerical equivalent. For example, if <id> were the number 123456790, then <url> would be the string "1234567890".


Mel Value Property

Let's get back to the basic Producer/Consumer example, and change the Consumer code to illustrate the use of the <value> property:
main () {
    <mel> int store;
    <!> Producer (store);
    <!> Consumer (store);
}
void Producer (<mel> int store) {
    while ( <?>store = Produce() );
}
void Consumer (<mel> int store) {
     while ( <?>store )
         Consume(store<value>);
}
In the original Consumer, we use a local variable, c, to temporarily hold the value read from the mel element store before we Consume it. In the above example, we skip this intermediary local variable, and access the read value directly via the <value> property.

The mel read statement, <?>store, suspends the Consumer task until the Producer task deposits a new item to the mel element. The Consumer task then removes this item from the mel element, and saves it in the <value> property of its mel variable. It then uses that property implicitly for the while checking for a zero value, and explicitly as argument for the Consume serial function.

The <value> property contains the mel value of Consumer's last read from the mel element. Since then the mel element may have been updated with a new deposit from the Producer task. Once this happens, the <value> property cached in the Consumer mel variable store will be different from the value at the mel element. To refresh its <value> property, the Consumer task has to read the mel element again, or use the snapshot operation.

The example above shows the <value> property as a simple entity (i.e. an integer). The <value> can represent more complex entities such as:


Mel Value Auto Operators

The auto operators are auto-increment and auto-decrement operators. For each auto operator, we also have the pre and post actions, resulting in four cases in total.

Let's take a look at this pre-auto-increment statement:
int c = ++<?>store;
These are the steps that are carried out:
  1. The task waits for the mel element represented by the mel variable store to be filled and available for the task to read.
  2. The mel element is emptied and its value is transferred to the task's <value>property.
  3. The <value> property is incremented.
  4. The incremented <value> property is assigned to the c variable.
Note that the auto-increment is done on the <value> property resident to the task, and not on the remote mel element. As a matter of fact, once a value is read from a mel element, it is gone from the mel element. A waiting writer task may fill the mel element right away, and this value is totally different, location-wise, from the value just read.

Now let's take a look at this post-auto-increment statement:
int c = <?>store++;
These are the steps that are carried out:
  1. The task waits for the mel element represented by the mel variable store to be filled and available for the task to read.
  2. The mel element is emptied and its value is transferred to the task's <value>property.
  3. The <value> property is assigned to the c variable.
  4. The <value> property is incremented.
The pre-auto-decrement and post-auto-decrement statements act in a similar way:
int c = --<?>store;
int c = <?>store--;
In both the pre and post decrement cases, the <value> property is decremented by 1 from the original value read from the mel element. The difference is that the value assigned to the variable c is after the decrement (for pre-auto-decrement) or before the decrement (for post-auto-decrement).


Mel Status Property

When a task does a read or write operation to a mel element, the status of the mel element after the operation is also returned and cached in the <status> property in the resident mel variable. Available statuses are:

StatusConstantSynopsis
OPENNERW_STATUS_OPENThe mel element has been successfully created, and can now be written or read.
EMPTYNERW_STATUS_EMPTYWhen the mel element buffer does not contain any value to be read, the mel element is said to be empty. The reader task has to wait for the mel element to be filled in order to acquire a value from the reader's side of the buffer.
FILLEDNERW_STATUS_FILLEDWhen the mel element buffer contains a value that can be read, the mel element is said to be filled. The reader task has been able to acquire this value from the reader's side of the buffer.
FULLNERW_STATUS_FULLWhen the mel element cannot receive another value to be written into it because all the slots in its mel buffer already have a value, the mel element is said to be full. The writer task has to wait for the mel element to become vacant before it can deposit a new value in the writer's side of the buffer.
VACANTNERW_STATUS_VACANTWhen the mel element has an available slot in its mel buffer for a value to be written into it, the mel element is said to be vacant. The writer task can deposit a new value in the writer's side of the buffer.
CLOSEDNERW_STATUS_CLOSEDThe mel element has been closed. The mel element cannot be read nor written any more.

The constant values can be bit-wise OR to form combinations. Some combinations are meaningful, such as a mel element can be both empty and vacant after a mel operation. Other combinations are not possible, such as being empty and full at the same time.

NERW_STATUS_OPEN

The OPEN status can be used to check if a mel creation has been successful or not. As discussed in Mel Creation, a programmer can check for the failure of a mel creation operation by using the <error> property or catching an exception. Checking the <status> for NERW_STATUS_OPEN is another way to detect failure:
<mel> int store;
if ( (store<status> & NERW_STATUS_OPEN) == 0x0 )
    printf ("Mel creation failed!\n");
The NERW_STATUS_OPEN status always exists along with NERW_STATUS_EMPTY, NERW_STATUS_FILLED, NERW_STATUS_FULL or NERW_STATUS_VACANT. It is mutually exclusive with NERW_STATUS_CLOSED since a mel element is either open or closed but not both.

NERW_STATUS_EMPTY / NERW_STATUS_FILLED

These two values reflect the status of the mel element for the next reading after a task's mel operation (read or write). With NERW_STATUS_EMPTY, the mel element does not have any value for the next reading. With NERW_STATUS_FILLED, the mel element has a value for the next reading. If the <status> property is set to NERW_STATUS_FILLED after a read operation, the mel element is most likely to be buffered, and that the next slot in the buffer already has a standing value. If the mel element is not buffered, after a read operation which removes the mel value, the return status is most likely be NERW_STATUS_EMPTY.

The previous paragraph uses "most likely" in several places due to different implementations of the CHAOS runtime. If CHAOS also takes into account waiting tasks, the returned status of a mel element will be opportunistic. For example, on the case of an unbuffered mel element after a read operation, the returned status will be NERW_STATUS_FILLED (instead of NERW_STATUS_EMPTY) if CHAOS detects that there is a writer task waiting to deposit its value and make the mel element filled again.

It is worth reminding that the <status> property reflects the status of the mel element after a mel operation by a task. It is not the current status of the mel element. For example, some time after a task does a mel read and gets back a NERW_STATUS_EMPTY status, a writer task has come in and deposit a new value, changing the current status of the mel element back to NERW_STATUS_FILLED. To refresh its cached <status> property, a task has to do another intrusive mel operation (such as a read or write), or invoke the non-intrusive snapshot operation.

It is also worth mentioning again that the <status> property is part of the mel variable and thus resident to a particular task. Two tasks, say A and B, access the same mel element and may end up with the same value for their <status> property; however these values represent different things: one is the status of the mel element after task A accesses the mel, and the other is the mel status after task B's access, which happens at a different time.

Let's change the Producer/Consumer example to illustrate the use of the <status> property for reading:
#define BUFFERSIZE 10
main () {
    <mel buffer=BUFFERSIZE> int store;
    <!> Producer (store);
    <!> Consumer (store);
}
void Producer (<mel> int store) {
    while ( <?>store = Produce() );
}
void Consumer (<mel> int store) {
    int products[BUFFERSIZE];
    int i, j;
    while ( 1 ) {
        for (i=0; i<BUFFERSIZE; ++i) {
            products[i] = <?>store;
            if ( store<status> & NERW_STATUS_EMPTY ) break;
        }
        for (j=0; j<=i; ++j) {
            if ( !products[j] ) break;
            Consume (products[i]);
        }
        if ( j != i ) break;     /* detect 0 value, break out of while loop */
    }
}
The mel element is now created with a buffer of BUFFERSIZE slots. Unlike the previous Consumer which does the consumption one product at a time, the new Consumer reads in all available products that are currently buffered. It knows when to stop reading by checking the <status> property for the NERW_STATUS_EMPTY condition. The Consumer task then Consumes all the collected products.

NERW_STATUS_FULL / NERW_STATUS_VACANT

These two values reflect the status of the mel element for the next writing after the task's mel operation (read or write). With NERW_STATUS_FULL, the mel element does not have an available slot to receive a new value. If the mel element is buffered, all of its slots currently contain a value. With NERW_STATUS_VACANT, the mel element is either empty or is buffered and has at least an available slot to receive a new writing.

Let's modify the Producer task to illustrate the <status> property for writing:
void Producer (<mel> int store) {
    int product = Produce();
    while ( product ) {
        <?>store = product;
        if ( store<status> & NERW_STATUS_FULL)  )
            slowdown();
        product = Produce();
    }
}
After each writing, this Producer checks the status of the mel element. If the mel element is not buffered (or in other words it has only one slot), and there is no reader task in the waiting, the status will be NERW_STATUS_FULL. If there is a reader task in the waiting, the status can be either NERW_STATUS_FULL or NERW_STATUS_VACANT depending on the implementation of the CHAOS runtime. If the mel element is buffered (i.e. having multiple slots), a NERW_STATUS_FULL means that all the slots have been filled up, and the next write can be hit by a wait for a slot to be freed. To allow a reader task to catch up, the Producer task invokes the slowdown function to slow down its production.

NERW_STATUS_CLOSED

Any task can invoke the <close> operation to close a mel element. Once this operation is successful, the <status> property in the mel variable resident to the task invoking the <close> operation will have the NEWS_STATUS_CLOSED bit set, due to the piggy-backing of the SET properties. Other tasks do not have their <status> property updated to NEWS_STATUS_CLOSED until they make a mel operation to the mel element.


Mel Condition Property

The <condition> property is similar to the <status> property, but is taken before the read or write operation can be applied to the mel element. It has the same values as the <status> property; however their interpretations are different.

After a reader task retrieves a value from the mel element, it can check the <condition> property for post-mortem analysis. If NERW_STATUS_FILLED is set, the reader task after being granted access to the mel element based on the read operation requested priority, has found the mel element to already contain a value for it to read away. If the condition bit NERW_STATUS_EMPTY is set instead, it means that the reader task has to wait for a writer task to deposit a value. It both cases, a read statement, such as <?>store can eventually succeed, but in the latter case, the <?> operator will suspend the reading task longer.

For a writer task, a NERW_STATUS_FULL <condition> means that the task has to wait in the mel writers' queue for a reader task to remove a value and make a slot available for deposit. On the other hand, a NERW_STATUS_VACANT means that the writer task can find a vacant slot to deposit its value, and quickly gets off the <?> wait operator and resumes its processing.

Let's modify the previous Producer task to use the <condition> property instead of the <status> property:
void Producer (<mel> int store) {
    int product = Produce();
    while ( product ) {
        <?>store = product;
        if ( store<condition> & NERW_STATUS_FULL )
            slowdown();
        product = Produce();
    }
}
The use of the <condition> property seems to be a better fit for the decision to slowdown the production than the use of the <status> property in the earlier example. No matter if the mel element is buffered or not, a mel wait in its just completed write means that the reader task (or tasks) cannot keep up with the writer task.


Mel Name Property

While the <id> and <url> properties identify the remote mel element, the <name> property is a character string that belongs to the mel variable. It contains a unique name that identifies the mel variable within its given scope. Among all the properties of a mel variable, the <name> property is unusual in a sense that it is a compile-time property instead of being a run-time one like the others. This means that any errors in using this property must be resolved when the NERWous C is still being compiled or translated.

Let's look at this code example:
<mel> int GlobStore1;
<mel as="Big Store"> int GlobStore2;
main () {
    <mel> int mainStore1;
    <mel as="Small Store"> int mainStore2;

    printf ("<name> value [%s] should be [GlobStore1]\n", GlobStore1<name>);
    printf ("<name> value [%s] should be [Big Store]\n", GlobStore2<name>);
    printf ("<name> value [%s] should be [mainStore1]\n", mainStore1<name>);
    printf ("<name> value [%s] should be [Small Store]\n", mainStore2<name>);

    foo1 (<?>mainStore1, <?> mainStore2);
    foo2 (<?>mainStore1, <?> mainStore2);
}
void foo1 (<mel> int argstore1, <mel as="Tiny Store"> int argstore2) {
    printf ("<name> value [%s] should be [argstore1]\n", argstore1<name>);
    printf ("<name> value [%s] should be [Tiny Store]\n", argstore2<name>);
}
void foo2 (<mel as="mainStore1"> int m1, <mel> int m2) {
    printf ("<name> value [%s] should be [mainStore1]\n", m1<name>);
    printf ("<name> value [%s] should be [m2]\n", m2<name>);
}
By default, the <name> property of a mel variable is simply its compile-time name under the scope rule. Mel variables with the default <name> property are GlobStore1 (in global scope), mainStore1, argStore1 and m2 (in local scope).

A programmer can change the default name by using the as attribute, as seen in the declarations for GlobStore2 (in global scope), mainStore2, argstore2 and m1 (in local scope).

Since the value of the as attribute must be resolved during compile time, it must be a constant value, and not a variable. For example, the code snippet below will generate a compilation error:
<mel as="MyMel1"> int store1;    /* OK */
#define MYNAME2 "MyMel2"
<mel as=MYNAME2> int store2;    /* OK */
char myname3[20] = {'M','y','M','e','l', '3', '\0' };
<mel as=myname3> int store2;    /* ERROR */

Mel Name Scope Rule

The names used as the values of the as attribute must obey the mel scope rule. It means that the same name cannot be re-used in the same scope. The following example shows the good and bad usages of the as name:
<mel> int GlobStore1;
<mel as="GlobStore1"> int GlobStore2;  /* ERROR 1: GlobStore1 is in use */
main () {
    <mel as="GlobStore1"> int mainStore1;  /* ERROR 1: GlobStore1 is in use */
    <mel as="mainStore1"> int mainStore2;  /* ERROR 2: mainStore1 is in use */

    foo1 (<?>mainStore1, <?> mainStore2);
    foo2 (<?>mainStore1, <?> mainStore2);
}
void foo1 (<mel as="GlobStore1"> int argstore1,  /* ERROR 1: GlobStore1 is in use */
           <mel as="mainStore2"> int argstore2) {  /* OK */
    printf ("[argstore1] name is [%s]\n", argstore1<name>);
    printf ("[argstore2] name is [%s]\n", argstore2<name>);
}
void foo2 (<mel as="mainStore1"> int argstore1,  /* OK */
           <mel as="mainStore2"> int argstore2) {  /* OK */
    printf ("[argstore1] name is [%s]\n", argstore1<name>);
    printf ("[argstore2] name is [%s]\n", argstore2<name>);
}
The ERROR 1 occurrences are due to the names of global mel variables are reserved globally. They can be used to create other mel variables that point to the same remote mel element (as in <snapshot> operation with on attribute), but not to assign as names to mel variables that point to different mel elements.

The ERROR 2 occurrence is due to the names of all local mel variables (such as mainStore1 in main) are reserved in the local scope. This is even though mainStore1 is declared with the as attribute to a different name. On the other hand, the argstore2 can use mainStore2 as name because mainStore2 is in the local scope of the main task but not of the foo1 function.

As said before, the errors due to as name usage incongruities must be resolved at compilation time. They cannot be caught with the <error> property or mel operation exceptions since they do not appear when the program is run.

Mel Name Reference

The use of the as attribute in the mel argument declarations of the functions foo1 and foo2 shows the mel element reference reason for supporting the <name> mel variable property. In previous chapters, we would write:
main () {
    <mel> int mainStore;

    <!> runParallel (<?>mainStore);
    runSerial (<?>mainStore);
}
void runParallel (<mel> int mainStore) {
    printf ("<name> [%s] should be [mainStore]\n", mainStore<name>);
    doThingsInParallel(<?>mainStore);
}
void runSerial (<mel> int mainStore) {
    printf ("<name> [%s] should be [mainStore]\n", mainStore<name>);
    doThingsInSerial(<?>mainStore);
}
Since a mel element, no matter in what compile-time scope it is created, is shared to all tasks during run time, it is customary (but not required) to use the same name for all the subsequent mel variables that access that common mel element. Thus, after the task main creates the mel element mainStore (and maintains the access via its resident eponymous mel variable mainStore), the argument to the task runParallel and to the function runSerial are also named mainStore. (See mel passing rule.)

There is no rule to keep the same name for mel variables. For example, there may be coding standards that dictate arguments to be named in a certain way; or some application programming interface (API) that suggests a certain consistency for argument naming. Thus, if the above example has to be written as follows:
main () {
    <mel> int mainStore;

    <!> runParallel (<?>mainStore);
    runSerial (<?>mainStore);
}
void runParallel (<mel> int pararg) {
    printf ("<name> [%s] should be [pararg]\n", pararg<name>);
    doThingsInParallel(<pararg>);
}
void runSerial (<mel> int m) {
    printf ("<name> [%s] should be [m]\n", m<name>);
    doThingsInSerial(<m>);
}
then to keep the reference to the same mel element, we use the as attribute as in this revised example:
main () {
    <mel> int mainStore;

    <!> runParallel (<?>mainStore);
    runSerial (<?>mainStore);
}
void runParallel (<mel as="mainStore"> int pararg) {
    printf ("<name> [%s] should be [mainStore]\n", pararg<name>);
    doThingsInParallel(<pararg>);
}
void runSerial (<mel as="mainStore"> int m) {
    printf ("<name> [%s] should be [mainStore]\n", m<name>);
    doThingsInSerial(<m>);
}
The mel variables mainStore, pararg and m, have the same CORE properties since they refer to the same remote mel element. To make this commonality visible at code level, the as values for pararg and m are set to the name of the remote mel element (i.e. mainStore).

Mel Name Usage

Besides the mel element reference, there are other uses for the mel name property. This list summarizes all of them:
  1. Mel element reference
  2. mel OR reads
In this chapter, we have discussed the mel name reference use. The other uses will be explored in subsequent chapters.


Previous Next Top

Friday, August 18, 2017

Pel Location

Welcome » NERWous C » Pel
  1. At Attribute
  2. Location Property
  3. Down Exception
  4. Import Attribute
  5. Array Variables


At Attribute

When a task is created via a simple pel statement:
<!>producer();
it is arbitrarily assigned to a computer element (cel) in a pool of cels maintained by the CHAOS runtime environment. There are times that the work requires a specialized hardware. Like the at attribute for mel creations, the at attribute can be used with pel creations to assign a computing "location" to a task:
extern <cel> VENDOR, DISTRIBUTOR, USER;
main () {
     <mel at=DISTRIBUTOR> int store;
     <! at=VENDOR> Producer (store);
     <! at=USER> Consumer (store);
}
void Producer (<mel> int store) {
     int c;
     while ( c = Produce() )
         <?>store = c;

     <close>store;
}
void Consumer (<mel> int store) {
    try {
         while ( 1 )
             Consume(<?>store);
     } catch ( store<CLOSED> ) {
         return;
     }
}
The cel entities VENDOR, DISTRIBUTOR and USER are defined as extern. Their definition will come from a configuration file that is associated with the above NERWous C program. This configuration file is specific to the runtime environment, and allows the same NERWous C program to be run on different physical platforms.

It is also possible to hard-code the value of the cel. Like the at attribute for the mel statement, the at attribute for the pel statement can accept either a cel value or a location value:
#define VENDORURL "https://pelvendor.nerw/"
#define USERURL "https://peluser.nerw/"

    <cel> VENDOR;
    VENDOR<id> = VENDORURL;

    <! at=VENDOR> Producer (store);
    <! at=USERURL> Consumer (store);
The Producer task uses the cel variable VENDOR like before. However this time, this cel is initialized within the program with VENDORURL as the identification value. The VENDOR cel can be initialized more succinctly:
    <cel> VENDOR = { VENDORURL };
The Consumer task specifies the identification value USERURL directly. The CHAOS runtime will automatically create a cel variable with this identification value, and assign it to the Consumer task.


Location Property

The pelling task and the pelled task can find out what is the identification of the cel in use via the location property.
extern <cel> VENDOR, DISTRIBUTOR, USER;
main () {
     <mel at=DISTRIBUTOR> int store;
     <pel> prod = <! at=VENDOR> Producer (store);
     <pel> cons = <! at=USER> Consumer (store);

     printf ("Producer at [%s]", prod<location>);
     printf ("Consumer at [%s]", cons<location>);
}
void Producer (<mel> int store) {
     printf ("Running at [%s]", pel<location>);
     int c;
     while ( c = Produce() )
         <?>store = c;

     <close>store;
}
void Consumer (<mel> int store) {
    printf ("Running at [%s]", pel<location>);
    try {
         while ( 1 )
             Consume(<?>store);
     } catch ( store<CLOSED> ) {
         return;
     }
}
The pelling task, main, uses the pel variables, prod and cons, in order to access the location property. For the pelled tasks, Producer and Consumer, the location property is accessed via the default pel variable which represents the running task.


Down Exception


main () {
    <pel> task_producer = <!> Producer ();
    <pel> task_consumer = <!> Consumer (task_producer);
}
int Producer () {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) {
           <return> c;
        }
        <release> c;
    }
}
void Consumer (<pel>tprod) {
    while ( 1 ) {
      try {
        consume (<?>tprod);
      }
      catch (tprod<ENDED>) {
        printf ("I am ending due to the Producer having ended");
        return;
      }
      catch (tprod<DOWN>) {
        printf ("I am ending due to the Producer has crashed");
        return;
      }
    }
}



Previous Next Top

Thursday, July 27, 2017

Pel Ending

Welcome » NERWous C » Pel
  1. Ways To End
  2. End Statement
  3. Terminate Statement
  4. Pel TERMED Exception
  5. Pel Ended Status
  6. Pel ENDED Exception
  7. Pel Ending Detection
  8. Kill Statement
  9. Pel Abortion
  10. Exit Statement


Ways To End

Once a task has been created, it has to end so that the program can exit. There are several ways for a task to end.
  • A task ends if it stops running. This is the normal behavior when the task runs off the last code statement. It implicitly invokes the end command.
  • A task ends when it invokes the end command. The task can specify if it ends normally or abnormally.
  • A task ends when it invokes the return statement. The return operation is the combination of the release operation and the end operation.
  • A task is terminated if it has ended orderly due to a terminate command from another task. The task receiving the terminate request can catch it via the TERMED exception to do an orderly ending.
  • A task is killed if it has ended abruptly due to a kill command from another task. The killed task will stop running on the spot.
  • A task is aborted if it has ended abnormally due to an uncaught exception. The aborted task will stop running on the spot.
When a task ends normally, it raises a ENDED exception. When a task ends abnormally, it raises the a FAILED exception. Both exceptions can be caught by tasks that interface with the ending task.

When all the tasks in a NERWous C program have ended, the NERWouc C program can exit.


End Statement

The end statement allows a task to end by itself. The end operation also supports the mode attribute to mark the ending status:

ModeSynopsis
ENDEDThis default mode is usually omitted. The task ends normally. Any tasks that are waiting on this task will get the ENDED exception.
FAILEDThis mode must be specifically specified. The task ends abnormally. Any tasks that are waiting on this task will get the FAILED exception.

Let's see the end statement in action with the following Producer/Consumer example, where the Producer code block is run in parallel with the Consumer code block.
#define MAX 100
main () {
   int counter = MAX;
   <mel buffer> int store;

   /* Producer task */
   <!> while ( --counter ) {
      int c = Produce();
      if ( c == 0 ) <end>;    /* explicit end */
      <?>store = Produce();
   }  /* implicit end */

   /* Consumer task */
   <pel> task_consumer = <!> {
      while ( --counter ) {
         try { Consume(<?>store); }
         catch ( store<NOWRITERS> ) { <end FAILED>; }
      }
      <end>;
   }

   /* Wait for Consumer to be done */
   try {
      <?> task_consumer;
   }
   catch ( task_consumer<ENDED> ) {
      printf ("All [%d] items processed fine", MAX);
   }
   catch ( task_consumer<FAILED> ) {
      printf ("Process ended early due to 0 value");
   }
}
The main task pels the Producer code block. This code block explicitly ends when it detects a 0-valued product and invokes the <end> statement. If no 0-valued product is ever generated, the Producer code block will run for MAX iterations; it then ends with an implicit <end> statement when it falls off the while loop and has no more code to run.

After pelling the Producer code block, the main pels the Consumer code block. The difference now is that main tracks the running of the Consumer code block with the pel variable task_consumer. There are two endings for the Consumer. It either ends with the status FAILED when it detects that there is no more Producer to feed the store mel variable, or it ends with the status ENDED when it has gone through MAX iterations, and explicitly invokes the <end> statement (unlike the Producer code block that invokes it implicitly).

The following statements are equivalent:
<end mode=ENDED>
<end ENDED>
<end>
After pelling the Consumer code block, the main task waits for it to end by waiting on the pel variable task_consumer. This wait is interrupted by either the task_consumer<ENDED> exception or the task_consumer<FAILED> exception.

In the above example, the end statements are used inside pelled code blocks. In subsequent examples, we will see the use of the end statement inside pelled functions.


Terminate Statement

In the Producer/Consumer example above, the Producer code block task keeps Produce'ing until a value 0 is generated; the Producer then ends. The Consumer code block task needs to detect that the Producer has ended, so that it can also end. However, not all Consumer tasks want to do this detection.

In the example below, we introduce another way to end the Consumer task. It depends on the main task that creates both the Producer and Consumer tasks to detect that the former has ended, and then to force the latter to end via the terminate command. Instead of inline code blocks, this time we will use the functions Producer and Consumer.
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    <terminate>task_consumer;
}
void Producer (<mel> int store) {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) <end>;
        <?>store = c;
    }
}
void Consumer (<mel> int store) {
    while ( 1 ) {
        consume (<?>store);
    }
}
The main task remembers the tasks it creates via the pel variables -- task_producer and task_consumer. It then waits for the task_producer to have ended by checking for its return value via the wait statement:
<?>task_producer;
This wait is over when the Producer runs the end statement to end itself. The main task then explicitly terminates the Consumer task via the statement:
<terminate>task_consumer;
The terminate operation on task_consumer does not mean that the main task ends the Consumer task abruptly. It is a request for Consumer to end graciously at the next NERWous C operation (either a mel operation or a pel operation). In the above example, it happens when Consumer does a mel wait <?>store.

If the Consumer task is already doing a mel wait on store, the terminate operation will result in the task being removed from the mel store wait queue so it can process the termination request. If the Consumer task is in the middle of running the consume function, the terminate command flags the task as having been requested for termination. When the Consumer task finishes with consume, it iterates the while loop and hits the mel wait statement again. This time, instead of accessing the mel store, it will process the termination request.

If the task targeted by the terminate operation never hits a mel or pel operation, it will not process a pending termination request. The task can end later via another ending method (such as invoking an end statement), or it can run indefinitely using only non-NERWous statements.

To announce its presence, the terminate request generates a pel TERMED exception against the targeted task. This task can catch this exception to end in an orderly manner (or not to end at all). The next section will show an example using the TERMED exception.

From the standpoint of the invoking task, issuing a terminate operation against another task that is already ended, will result in an immediate success. Otherwise, the terminate operation does not wait for the targeted task to be ended. As long as the termination request has been successfully registered at the targeted task, the invoking task can continue on with its execution thread. If the invoking task later wants to check if the targeted task has indeed ended, it can check for the latter's <ENDED> status, as will be discussed in the later pel status section.


Pel TERMED Exception

In the example above, the Consumer task terminates without the chance for any self-ending work. For an orderly ending, the Consumer task can trap the TERMED exception raised by the terminate operation:
void Consumer (<mel> int store) {
 while ( 1 ) {
     try {
         consume (<?>store);
     }
     catch (pel<TERMED>) {
         printf ("I'm being terminated");
         <end>;
     }
 }
}
The ending work is done inside the catch (pel<TERMED>) code block. The keyword pel represents the current task (i.e. the Consumer task).

The Consumer task checks for the pel<TERMED> exception whenever it makes use of a mel operation (such as a mel wait), or a pel operation, inside the try block.

Once the Consumer task enters the catch code block, the pel TERMED exception is deemed to have been serviced. Thus, invocations of any NERWous C mel and pel operations are now available, without re-triggering the pel TERMED exception. A targeted task could take advantage of this clearance to continue running. However, the purpose of the TERMED catch code block is to tidy up the task before terminating -- it is not intended to have the task ignoring the terminate request. The catch (pel<TERMED>) code block should end with an end statement, which allows the task to end itself, as in the Consumer example above.

If Consumer were to omit the end statement inside the pel<TERMED> code block, it would acknowledge the termination request but would not end. For example:
void Consumer (<mel> int store) {
 while ( 1 ) {
     try {
         consume (<?>store);
     }
     catch (pel<TERMED>) {
         printf ("I'm being terminated");

         /* <end>; */
         printf ("No return - I'm ignoring the terminate request");
     }
 }
}
In the above code, the while ( 1 ) loop would iterate, the mel wait <?> would be reached again, but this time, since the TERMED exception has been serviced, the mel wait operation would not re-trigger the execution of the pel<TERMED> code block, and the Consumer would continue to run.


Pel Ended Status

Let's get back to the main task. After issuing
<terminate>task_consumer;
the main task ends. However the program can only exit if all the tasks that are pelled within the program have also ended. With the Consumer task being able to delay or even ignore the termination request, main can check for the ENDED status of Consumer to see if that it has terminated:
#include "nerw.h"
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    printf ("Task Producer ending detected by waiting on pel variable");
    <terminate>task_consumer;
    sleep (60);      /* give Consumer 1 min to end graciously */
    <update>task_consumer;     /* update the local properties */
    if ( task_consumer<status> != "NERW_STATUS_ENDED" )
        printf ("Task Consumer still has not ended after 1 min");
}
Assuming that 60 seconds is ample enough for Consumer to wrap up and end, if main detects that the former is still running, it displays the "not ended" statement. In this example, main then ends. However if Consumer is still running, the whole program cannot exit. Later we will introduce the kill statement where main can kill the runaway task so that the whole program can exit.


Pel ENDED Exception

There are several ways for Consumer to know that it should end. From the mel variable store that it shares with Producer, it can check for the CLOSED exception or the NOWRITERS exception.

From the pel side of NERWous C, Consumer receives a termination request from the pelling task triggering a TERMED exception. We now add another way which is for the interfacing task to have ended, triggering an ENDED exception.

In the example below, let's combine all the ways:
main () {
    <mel> int store;
    <pel> task_producer = <! name="TheProd"> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    <terminate>task_consumer;
}
void Producer (<mel> int store) {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) {
            <close>store;
            <end>;
        }
        <?>store = c;
    }
}
void Consumer (<mel> int store) {
    <pel> task_producer;
    <update name="TheProd">task_producer;
    while ( 1 ) {
        try {
            consume (<?>store);
            <update>task_producer;
        }
        catch (store<CLOSED>) {
            printf ("I am ending due to a MEL CLOSURE");
            <end>;
        }
        catch (store<NOWRITERS>) {
            printf ("I am ending due to no MEL SUBSCRIPTION");
            <end>;
        }
        catch (task_producer<ENDED>) {
            printf ("I am closing due to a PEL CLOSURE");
            <end>;
        }
        catch (pel<TERMED>) {
            printf ("I am ending due to a PEL TERMINATION");
            <end>;
        }
    }
}
The Consumer task needs to discover the task Producer in order to catch its ENDED exception. It does that by asking the CHAOS environment to scan for a task named TheProd, via the update statement:
<update name="TheProd">task_producer;
Once the local variable task_producer is established, Consumer can download the task Producer's properties, including its status, via the simpler update statement without the name attribute. This download is done in every while iteration. If the status shows an ENDED condition, the task_producer<ENDED> exception is triggered.

The Consumer task is also catching three other exceptions. On a 0-valued product, the Producer task invokes the <close>store operation, generating the mel closure exception (store<CLOSED>) at the Consumer task. Producer then ends itself with the end statement, thus removing itself from the mel store writers subscription. Since there is no other writer task, the store<NOWRITERS> exception can also be raised at the Consumer.

By watching the Producer task via the pel wait statement <?>task_producer the main task can detect that the Producer has ended. It then issues a terminate request to Consumer, triggering the pel termination exception (pel<TERMED>).

Chronologically, the mel closure exception is generated first. followed by the mel no-writers exception, then the pel ended exception, finally followed by the termination exception. However what exception the Consumer takes depends not only on the particular CHAOS runtime environment implementation, but also on the timing of the tasks involved.

For example, if Producer ends while Consumer is doing the consumption of the last store value, when Consumer is done with this consumption and checks on Producer via the update operation, the pel task_producer<ENDED> exception will be caught. However if Producer has ended after Consumer has checked on it, when Consumer iterates the while loop to get the next value of store, the mel closure store<CLOSED> exception will be caught instead.

A NERWous C exception can only be detected when a task is doing a corresponding NERWous C operation. When a task does a mel operation, it first checks for any termination exception, and if none, it checks for any mel exception, then any pel exception. When a task does a pel operation, it first checks for any termination exception, and if none, it checks for any pel exception then any mel exception.

In the above example, all the exception handlers end with an end statement. If end is missing (unintentionally or on purpose), then the subsequent behavior of the task will be different. The pel<TERMED> exception is an one-off exception: once it is serviced by a catch block, the exception is removed. On the other hand, the mel CLOSED and NOWRITERS exceptions and the pel ENDED exception are persistent. If the end statement is missed, on the next iteration of the while loop, the mel closure exception will be caught again when the task tries to access the mel variable at the <?>store wait statement. The task will spin ad nauseam in this manner until it is terminated or killed. To allow a termination to stop this potential live lock, the termination request is checked on entry of each mel or pel operation before any other exception.

In practice, not all the ending detection methods need to be used by a task. The task just selects one method that best fits its logic.


Pel Ending Detection

We now explore the behind-the-scene flow of the many ways using a pel variable to check if a task has ended:
  • Wait for a pel feedback
  • Check the pel status
  • Inquire a pel exception
The following function will do all three of them:
function monitorTaskEnd (<pel> atask) {
    printf ("Wait for pel feedback");
    <?> atask;
    printf ("[atask] has ended based on the pel feedback");

    printf ("Check pel status");
    if ( atask<status> == "NERW_STATUS_ENDED" )
        printf ("[atask] has ended based on the status check");

    printf ("Inquire pel exception");
    try {
        atask<ENDED>;
    }
    catch ( atask<ENDED> ) {
        printf ("[atask] has ended from the pel ENDED exception");
    }
}
Wait for a pel feedback

The statement
<?> atask;
suspends the invoking task until atask either releases a value or has ended.

For tasks that do not release value, such as the ones in this chapter, the pel feedback wait is only broken if the tasks end. For tasks that keep releasing new values, the pel feedback wait is also broken when the tasks "stream" out new values. Thus the breakoff of the pel feedback wait by itself does not allow us to know if the tasks have ended or not. However the pel feedback operation allows the update operation to piggy-back, and the <status> property can be checked to see if the tasks have ended. This is what monitorTaskEnd does above.

Check the pel status

The statement
if ( atask<status> == "NERW_STATUS_ENDED" )
checks the <status> property that is locally cached. This property is updated from the last interaction with the task presented by the pel variable atask. This property may be stale since it does not represent the actual status of the task.

To get the current status, the monitoring task can invoke the update operation explicitly. In addition, all pel operations also piggy-back the update operation implicitly. The latter is what monitorTaskEnd makes use. It depends on the pel feedback operation to re-stock all the pel variable properties, including the <status> one.

Inquire a pel exception

The statement
atask<ENDED>;
sends a status request to the targeted pel, waits for the reply back, updates the local pel properties, and raises the ENDED exception if the returned status indicates that the task has ended. Thus this statement is normally run inside a try / catch construct, with a corresponding <ENDED> exception handler. If run outside a try / catch, or if there is no corresponding handler, an unhandled <ENDED> exception will cause the invoking task to abort.


Kill Statement

Let's go back to an earlier example where the main task issues a request for Consumer to terminate:
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    <terminate>task_consumer;
}
Upon registering the request to the Consumer task, the terminate command is done, and main flows to the next statement. Since there is no more statement, the task main ends. However for the program to exit, the Producer and Consumer tasks must also have ended.

As discussed previously, the Consumer task can take its time to end, or even may not go through with the ending. If main wants to enforce the ending, it has to use the <kill> statement. For example, after issuing the terminate request, main can wait for 60 seconds. If it does not detect the ENDED status after that time frame, it issues the kill command:
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    <terminate>task_consumer;
    sleep (60);      /* give Consumer 1 min to close graciously */
    <update>task_consumer;
    if ( task_consumer<status> != NERW_STATUS_ENDED ) {
        printf ("Task Consumer still has not closed after 1 min");
        <kill> task_consumer;
    }
    
}
The kill command is different from the terminate command in many ways:
  1. The kill command does not allow the targeted task to clean up. The kill signal is sent to the CHAOS runtime directly. CHAOS immediately ends the targeted task.
     
  2. While terminate does not wait for the task to be terminated, the kill command will suspend the calling task until CHAOS has reported back that the targeted task has ended.

Kill All Statement

The simple kill command only kills the specified task, not its children tasks. If the killing task wants to kill a task, its children tasks, and other subsequent descendant tasks, it should use the all attribute. For example:
<kill all> task_consumer;


Pel Abortion

There is another way for the task to end. The task can end via an abortion. This happens when there is a mel, pel or task exception, but the task code does not have a corresponding catch handler. For example:
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    <terminate>task_consumer;
}
void Producer (<mel> int store) {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) {
            <close>store;
            <end>;
        }
        <?>store = c;
    }
}
void Consumer (<mel> int store) {
    while ( 1 ) {
        consume (<?>store);
    }
}
We have removed the try / catch handler from the Consumer task. Therefore, when the Producer task closes the mel store, there is no handler at Consumer to process that exception. Likewise, there is no handler at Consumer to process the termination request from the main task.

Whenever the Consumer task accesses the CHAOS runtime environment for a mel or pel operation, it will check for any pending exception. If one is found and there is no corresponding handler. the task will end on the spot. This is called ending via task abortion.


Exit Statement

The NERWous C examples introduced so far have exited without any exit value. An exit value is required if the NERWous C program is used in a batch or a pipeline of interrelated programs, so that the result of its run can be monitored and dictates the rest of the processing.

C programs invoke the exit command to return an exit value. The corresponding NERWous C is the <exit> command. Let's modify the main task to use this command.
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);

    <?>task_producer;
    <terminate>task_consumer;
    <exit> 0;
}
By popular convention in many operating systems, a zero exit value means that the program has run successfully. Any non-zero value, either positive or negative, means that the program execution has an issue.

The NERWous C <exit> statement first ends the task that calls it, then has the CHAOS runtime ends any task that is still running. Once all tasks have ended, the NERWous C <exit> calls the C language exit command to exit the program with the specified exit value.

How does the CHAOS runtime ends all the tasks? It uses either the kill command or the terminate command.

Exit Now Statement

The <exit> statement supports the now attribute to force the program to exit right away. It does that by killing any task still running, so that all tasks will end right away but abruptly:
<exit now> 0;

Exit ASAP Statement

The <exit> statement supports the asap attribute to allow the program to exit as soon as possible. It does that by terminating any task still running. Termination allows the tasks to close more gracefully, especially if they have a TERMED exception handler.
<exit asap> 0;
The exit with asap may not exit the program right away or ever, if a task, by design or not, chooses to ignore the termination request and continues to run.

The default behavior when no attribute is added, is the asap behavior. Thus, the following statements are equivalent:
<exit asp>;
<exit>;

Multiple Exit Statements

What happens if more than one task invokes the <exit> statement? Consider this example:
main () {
    <!> runthis();
    domain ();
    <exit> 0;
}

function runthis () {
    dothis();
    <exit> 1;
}
What is the exit value of the program? Is it 1 from runthis, or is it 0 from main? The answer is that it can be either way, depending on which <exit> statement is invoked last. The exit value of the program is the exit value of the last <exit> statement.

If main runs domain() fast enough, it will invoke its <exit> statement with the 0 exit value first, which causes it to end. Since the task runthis is still running, it will receive a terminate request. However since this task is not doing any mel or pel operation, it does not honor the terminate request. It finishes doing dothis and invokes its own <exit> statement with 1 as the exit value. This causes runthis to end. With all the tasks having ended, the program exits with the value of the last <exit> statement, which is 1 from runthis.

Using the same logic as above, if dothis run faster, and runthis issues the <exit> statement first, the program will exit with the 0 value from the main task.

ALthough supported, it is not recommended to have multiple <exit> statements in a NERWous C program. The children tasks should use <return> statements to send back the status of their run to the main task. The main task then invokes the <exit> statement:
main () {
    <pel> p = <!> runthis();
    domain ();
    <exit> <?>p;
}

function runthis () {
    <return> dothis();
}
The next chapter will discuss the <return> statement.


Previous Next Top

Pel Returns

Welcome » NERWous C » Pel
  1. Return Statement
  2. Release Statement
  3. Multi-Value Release
  4. Multi-Value Return


Return Statement

The return statement is used by a task to end itself with a return value. It is a combination of the <release> statement which does the value return, and the <end> statement which does the task ending. The <return> statement is the NERWous C embodiment of the C language return statement.

The return statement causes the ending task to generate a special return value, This return value is a readonly mel. By nature of a readonly mel, other tasks can all read this return value at the same time whenever it is made available, or wait for it if not yet available. By waiting on the return value of a task, the inquiring tasks can detect that the targeted task has ended, as the main task is doing below:
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);
    
    <?>task_producer;
    <terminate>task_consumer;
}
void Producer (<mel> int store) {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) {
            <end>store;
            <return>;
        }
        <?>store = c;
    }
}
void Consumer ( int store) {
   while ( 1 ) {
     try {
         consume (store);
     }
     catch (pel<TERMED>) {
         printf ("I'm being terminated");
         return;
     }
   }
}
The wait that the main task is doing on <?>task_producer is the same pel feedback wait we have encountered in the previous Pel Endings chapter. This wait will be broken if the task_producer has ended via the <return> statement as seen in the Producer function code above.

Since the return signature of the function Producer is a void, the return value of task_producer is indefinite. Inquiring tasks can wait on this indefinite value to detect that a task has ended, but should not make use of it for any assignment or consumption. When there is no real value to be returned, the <return> statement acts simlarly like the <end> statement. This allows the same pel feedback wait to serve both statements.

Let's change the Producer function so that it returns an actual value to make the use of the <return> statement worthwhile:
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store);
    
    printf ("Task Producer has ended with value [%d]", <?>task_producer);
    <terminate>task_consumer;
}
int Producer (<mel> int store) {
    while ( 1 ) {
    int c = produce();
        if ( c == 0 ) {
            <end>store;
            <return> c;
        }
        <?>store = c;
    }
}
The main task now waits for the Producer task to end, and also makes use of the return value (as seen in the printf function call).

Since the return value of a task is a readonly mel, multiple tasks can access it. For example, let's the Consumer task access the Producer return value too:
main () {
    <mel> int store;
    <pel> task_producer = <!> Producer (store);
    <pel> task_consumer = <!> Consumer (store, task_producer);
    
    printf ("Task Producer has ended with value [%d]", <?>task_producer);
    <?>task_consumer;
    printf ("Task Consumer has also ended");
}
int Producer (<mel> int store) {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) {
            <close>store;
            <return> c;
        }
        <?>store = c;
    }
}
void Consumer (<mel> int store, <pel>tprod) {
    int c;
    while ( 1 ) {
      try {
        c = <?>(store || tprod);
        if ( c == 0 ) <end>;
        consume (c);
      }
      catch (store<CLOSED>) {
        printf ("I am ending due to a MEL CLOSURE");
        return;
      }
      catch (tprod<ENDED>) {
        printf ("I am ending due to the Producer having ended");
        return;
      }
    }
}
In this new version, the main task does not terminate the Consumer task any more. Instead, the latter will detect that the Producer has ended by accessing its return value, and then ends itself with its own <end> statement. With the statement <?>(store || tprod), the Consumer waits for either a value in the mel store, or the return value of the Producer task.

While both the main task and Consumer task wait on the return value of Producer, there is a difference between their wait. The wait from main is a single mel read wait while the wait from Consumer is a mel OR read wait. Normally Consumer waits on store being valued. However when Producer has ended, the store mel stops being valued; instead Consumer will pick up the return value of the ending Producer. When it sees the 0 value of the return value, the Consumer will explicitly ends with its own <end> statement.

Since the main does not invoke terminate on the Consumer task any more, the catch (pel<TERMED>) has been removed from the catch exception repertoire of the Consumer task. (It can be kept for reference but there is no event to trigger it.) It has been replaced by a new exception handling:
catch (tprod<ENDED>) {
    printf ("I am ending due to the Producer having ended");
    return;
}
There are now two methods for Consumer to detect that the Producer has ended: checking its return value via the pel feedback wait or getting the pel ENDED exception. With both methods present, the return value method takes precedence. The Consumer first determines if the return value is empty before it checks for any pel exception. In the Producer/Consumer example above, when the Producer ends, it generates a return value, causing the ENDED exception handler at Consumer not to be exercised.

Although playing a subservient role to the pel feedback wait, the ENDED exception handling is more general. It can handle the case where Producer has ended graciously with a proper return value, as well as the case where Producer has crashed without releasing any return value. As a matter of fact, we can remove the check for the 0-value (if ( c == 0) return) and make Consumer more succinct:
void Consumer (<mel> int store, <pel>tprod) {
    while ( 1 ) {
      try {
        consume ( <?>(store || tprod) );
      }
      catch (store<CLOSED>) {
        printf ("I am ending due to a MEL CLOSURE");
        return;
      }
      catch (tprod<ENDED>) {
        printf ("I am ending due to the Producer having ended");
        return;
      }
    }
}

Serial Return In Parallel

Sharp-eyed readers would notice that the exception handlers in the Consumer task use the C language return statement instead of the NERWous C <return> statement. This is done on purpose to illustrate that once a serial function is pelled to run in parallel, its return statement is transformed by the CHAOS runtime into the <return> statement. This allows simple functions to be invoked serially or in parallel, without having two versions:
int SimpleConsume (int c) {
    if ( !consume (c) ) return 1;    /* error from consume */
    return 0;    /* consume is successful */
}
int rescon;
rescon = SimpleConsume (10);    /* invoke serially */
rescon = <!> SimpleConsume (0);    /* pel to run in parallel */
In the second invocation, SimpleConsume runs as a pelled task. Its return statement then becomes <return> behind-the-scene. Without this automatic conversion, we would have to create a NERWous C version of SimpleConsume that used <return> in order to run it in parallel.

Parallel Return In Serial

Once a function makes use of the <return> statement, its invocation has unintended consequence when run serially:
int SimpleConsumeTask (int c) {
   if ( !consume (c) ) <return> 1;
   <return> 0;
}
main () {
    int rescon;
    rescon = <!> SimpleConsumeTask (0);    /* OK when pelled */
    rescon = SimpleConsumeTask (10);    /* Watch out when invoked */
}
The first invocation of <return> inside the task SimpleConsumeTask is what is intended - it ends this task and returns a value to the task main. The second invocation from a serial call to the function SimpleConsumeTask is done in the context of the main task, so when <return> is run in SimpleConsumeTask, it is not a return from that function, but a <return> from the main task, causing it to end.

Pel'N'Back Shortcut

This statement seems to be buggy:
rescon = <!> SimpleConsumeTask (0);
The left-hand-side of a pel <!> operator should be a pel variable, which rescon is not. However the above statement is all right since it is a shortcut of:

<pel> _p0 = <!> SimpleConsumeTask (0);
rescon = <?> _p0;
This shortcut is called pell'n'back because after pelling the task, the parent task immediately goes to the pel feedback wait. The required pel variable is not visible, it is created behind-the-scene, and is transient as a one-time use.

The pel'n'back shortcut is used when the code to be pelled has been written for task pelling, such as containing the <return> statement.

Return For Inline Code

Earlier, we see that return and <return> can be used interchangeably inside a pelled function. However if the pelled code is an inline block code, then only the NERWous C format of <return> is supported.

Let's put the Producer and Consumer functios as code blocks inside the main function:
main () {
    <mel> int store;
    
    /* Producer coded inline */
    <pel> task_producer = <!> {
        while ( 1 ) {
            int c = produce();
            if ( c == 0 ) {
               <end>store;
               <return inline> c;
            }
            <?>store = c;
        }
     }

    /* Consumer coded inline */
    <pel> task_consumer = <!> {
        while ( 1 ) {
          try {
             consume ( <?>(store || task_producer) );
          }
          catch (store<CLOSED>) {
             printf ("I am ending due to a MEL CLOSURE");
             <return>;
          }
          catch (task_producer<ENDED>) {
             printf ("I am ending due to the Producer having ended");
             <return>;
          }
        }
    }
        
    printf ("Task Producer has ended with value [%d]", <?>task_producer);
    <?>task_consumer;
    printf ("Task Consumer has also ended");
}
The serial format return are not permitted for the pelled inline codes because return works with functions and the inline code blocks are not functions.

The inline attribute used in the producer inline code is optional. It is a cosmetic reminder that the <return> statement is to be applied to the inline code block and not to the parent task (i.e. main). Since inline is cosmetic, the consumer inline code opts not to use it in its exception handlers. As a matter of style, it is recommended that the inline attribute should be used here as well.

The local variables store and task_producer are defined in the main task and passed over to the consumer inline task, as described in Local and Global Variables.


Release Statement

In the previous examples, we use the store mel variable to pass a product from the Producer task to the Consumer task. Another way is for the Producer to "stream" its products directly to the Consumer without using the store intermediary. This is supported by the NERWous C <release> statement:
main () {
    <pel> task_producer = <!> Producer ();
    <pel> task_consumer = <!> Consumer (task_producer);
    
    <?>task_producer<ENDED>;
    printf ("Task Producer has ended with value [%d]", <?>task_producer);
    <?>task_consumer<ENDED>;
    printf ("Task Consumer has ended");
}
int Producer () {
    while ( 1 ) {
        int c = produce();
        if ( c == 0 ) {
           <return> c;
        }
        <release> c;
    }
}
void Consumer (<pel>tprod) {
    try {
        consume (<?>tprod);
        <resume>;
    }
    catch (tprod<ENDED>) {
        printf ("I am ending due to the Producer having ended");
        return;
    }
}
The <release> statement writes to the same readonly mel return value used by the <return> statement. The difference is that <return> follows the mel write with a task ending, while the <release> lets the task resume processing. In the Producer code above, the task keeps invoking the release of newly produced items, until a zero-valued item is produced; at that time, the Producer task does a <return> on that last item.

The Consumer task gets newly produced items directly from the readonly mel return value of the Producer task. Instead of checking every time for a zero-valued item, it depends on the ENDED exception to know that the Producer task has ended.

The main task has a major change in how it detects that Producer has ended. Previously, it waits on the readonly mel return value of Producer since the latter only writes to this location once, at its end time. Now with this return value used as a streaming channel, main has to wait for Producer to have the ENDED status:
<?>task_producer<ENDED>;
The main task could still use the checking for the mel return value to detect the ending of Consumer since it only does a <return> and no <release>. However we also change the code for main to wait for the ENDED status of the Consumer task, to be consistent with the Producer detection.

The main task does the wait on the tasks ENDED status because it needs to do the printf statements. If it does not have to do printf, then there is no need for the explicit wait statements, and the main code would be simplified to:
main () {
    <pel> task_producer = <!> Producer ();
    <pel> task_consumer = <!> Consumer (task_producer);
}
After the two pel statements, the main task ends. When the Producer and Consumer tasks also end, the whole program exits.


Multi-Value Release

NERWous C supports a feature that is not part of the NERW Reference Model but can make writing some concurrent and serial programs easier: the capability to return multiple values. As we all know, the classic C language allows only one value to be returned.

Let's modify our example so that the Producer task produces one premium product for every 5 standard products, until over 1000 items are produced. We will have two consumer tasks, one to consume the premium products, and the other, the standard products:
main () {
    <pel> task_producer = <!> Producer ();
    <pel> task_consumer_std = <!> Consumer_Std (task_producer);
    <pel> task_consumer_prem = <!> Consumer_Prem (task_producer);
    
    <?>task_producer<ENDED>;
    printf ("Task Producer has ended with standard value [%d] and premium value [%d]", 
             <?>task_producer.std, <?>task_producer.prem);
    <?>task_consumer_std;
    <?>task_consumer_prem;
    printf ("Both Consumer tasks have ended");
}

(int prem, int std) Producer () {
    int count = 0;
    while ( count < 1000 ) {
        for (int i=0; i<5; ++i)
            <release name=std> produce_standard();
        <release prem> produce_premium();
        count += 6;
    }
    <release> (0, 0);
}
void Consumer_Std (<pel>tprod) {
    try {
        consume_std (<? name="std">tprod);
        <resume>;
    }
    catch (tprod<ENDED>) {
        printf ("I am ending due to the Producer having ended");
        return;
    }
}
void Consumer_Prem (<pel>tprod) {
    try {
        consume_prem (<? prem>tprod);
        <resume>;
    }
    catch (tprod<ENDED>) {
        printf ("I am ending due to the Producer having ended");
        return;
    }
}
The Producer now returns two readonly mel values, prem and std. These names are declared in the Producer function signature. The Producer task can release individual values by specifying their name, such as in:
<release name=std> produce_standard();
The name attribute can be omitted, as in:
<release prem> produce_premium();
When all the return values are specified, the <release> statement does not require the name attribute, as in:
<release> (0, 0);
Each Consumer task can wait for its own preferred product by specifying the name of the readonly mel value, as in consume_std (<? name=std>tprod) or consume_prem (<? prem>tprod).


Multi-Value Return

In the above example, the Producer task does a <release> with all the readonly mel values as the last statement in the function code block. Since it runs out of code to run, it will end. The equivalent is to use the <return> statement to signal the intention to terminate:
(int prem, int std) Producer () {
    int count = 0;
    while ( count < 1000 ) {
        for (int i=0; i<5; ++i)
            <release name=std> produce_standard();
        <release prem> produce_premium();
        count += 6;
    }
    <return> (0, 0);
}
Unlike the <release> statement, all the returned values of the function must be specified in a <return> statement. Any missing value will result in a syntax error during NERWous C translation time.

The multi-value feature is only supported for the NERWous C <return> statement. NERWous C leave it open for an implementation to extend that feature to standard C language <return> statement.


Previous Next Top