Pages

Friday, September 6, 2019

Mel Combined Reads

Welcome » NERWous C » Mel
  1. Mel Multiple Reads
  2. Mel OR Reads
    1. Parallel OR Read
    2. Serial OR Read
    3. OR Read Methods Summary
  3. OR Read Exceptions
    1. OR Read TIMEDOUT Exceptions
    2. OR Read CLOSED Exceptions
  4. Asynchronous Mel OR Reads
  5. Mel AND Reads
  6. Cumulative AND Reads
    1. Cumulative AND Reads Timeouts
    2. Cumulative AND Reads Closeouts
  7. Simultaneous AND Reads
    1. Simultaneous AND Reads Timeouts
    2. Simultaneous AND Reads Closeouts
  8. Asynchronous Mel AND Reads
    1. Error Handling Asynchronous AND Reads
    2. Exception Handling Asynchronous AND Reads


Mel Multiple Reads

Let's create a greedy Consumer that Consumes from two Producer tasks at the same time:
main () {
     <mel> int store1;
     <mel> int store2;
     <!> Producer (store1);
     <!> Producer (store2);
     <!> Consumer (store1, store2);
}
void Producer (<mel> int store) {
     while ( <?>store = Produce() );
     <close>store;
}
void Consumer (<mel> int store1, <mel> int store2) {
     bool store1valid = true;
     bool store2valid = true;

     while ( 1 ) {
        if ( store1valid ) {
           try {  Consume(<? timeout>store1); }
           catch ( store1<TIMEDOUT> ) {}
           catch ( store1<CLOSED> ) { store1valid = false; }
        }

        if ( store2valid ) {
           try {  Consume(<? timeout>store2); }
           catch ( store2<TIMEDOUT> ) {}
           catch ( store2<CLOSED> ) { store2valid = false; }
        }

        if (!store1valid && !store2valid) break;     // no more producers
     }
}
The Consumer uses a default timeout in both its mel reads. If the first Producer is too slow, the timeout exception fires and allows the Consumer to break out from the mel wait. It then tries the other Producer.

The Consumer gets a CLOSED exception if the corresponding Producer has closed the mel element. The Consumer remembers this CLOSED status in a local variable. On future iterations, the Consumer skips the closed mel element and works with the other Producer only. When the Consumer has detected that both mel elements have been closed, it will break out of the infinite loop and ends itself.

For a more compact code albeit less efficient, we can check the mel property CLOSED instead of using a local variable:
/* VERSION A */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try { Consume(<? timeout>store1); }
    catch ( store1<TIMEDOUT> ) {}
    catch ( store1<CLOSED> ) {}

    try { Consume(<? timeout>store2); }
    catch ( store2<TIMEDOUT> ) {}
    catch ( store2<CLOSED> ) {}

    if (store1<CLOSED> && store2<CLOSED>) break;
  }
}
The cleaner code is easier to follow. The price paid is a performance reduction. Even though a mel element has been closed, the Consumer will continue to check it at each iteration and immediately gets the CLOSED exception that it has to handle again. All this repetitive computation takes more time than simply checking a local variable that "remembers" the closed status.

A CLOSED or TIMEDOUT exception to a mel wait is cleared when the task jumps out of the corresponding handler. For example, if Consumer has detected that store1 has closed, the store1<CLOSED> exception is raised during that iteration. The corresponding CLOSED handler does not have any visible code, but it does serve two purposes: it prevents the exception to become escalated, and it clears the exception. On the next iteration of the loop, when Consumer invokes Consume(<? timeout>store1) again, the store1<CLOSED> exception is raised again when the mel wait detects that the mel element has indeed been closed. This back-and-forth processing continues until the Consumer task detects that both mel elements have been closed. Only at that time, it breaks out of the while loop, and ends.


Mel OR Reads

The Version A of Consumer is very deterministic: it Consumes store1 then store2. If a mel element is empty, it depends on the TIMEDOUT exception to stop the wait on that mel element and switch to the other mel wait. In Version B below, the reading from two (or more) elements can be written more compactly using the mel OR read feature.
/* VERSION B */
void Consumer (<mel> int store1, <mel> int store2) {
  int c;
  while ( 1 ) {
    try {
      c = <? timeout>(store1 || store2);
      Consume(c);
    }
    catch ( (store1 || store2)<TIMEDOUT> ) { printf ("Wait too long"); }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
An even more compact version of Consumer removes the intermediary local variable c:
/* VERSION C */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try { Consume(<? timeout>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) { printf ("Wait too long"); }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
Parallel OR Read

The default method for a mel OR read is the parallel method. This method checks all the listed mel elements at the same time, and pick the first one that is read ready.

This is its behind-the-scene process for the sample statement below:
<? timeout>(store1 || store2):
  1. The Consumer task puts itself to both store1 and store2 default readers' queues with the default timeout value.
     
  2. If the mel READ conditions are met at, for example store1, the Consumer retrieves the mel value from that mel buffer, gets off the store1 queue, aborts its wait on the store2 mel by getting off the store2 queue, exits the mel OR wait statement, and Consumes the store1 retrieved mel value. Similar processing is done if the mel READ conditions are met first at store2.
     
  3. If the mel READ conditions are not met at neither store1 nor store2, the Consumer will wait on both queues. When the mel READ conditions become true for either one of the mel variables, the Consumer task retrieves the mel value from that mel buffer, gets off both queues, exits the mel OR wait statement, and Consumes the retrieved mel value.
     
  4. If it happens that the mel READ conditions become true at both mel elements at the same time, the CHAOS runtime will ensure that Consumer will retrieve value from one or the other mel element, and not both.
     
  5. If the mel READ conditions are not met at neither store1 nor store2 by the timeout period, the Consumer task will get off both queues, and raises the <TIMEOUT> exception. This will be explored in a later section.
Serial OR Read

NERWous C supports another method called the OR serial read method. Here, the first listed mel is checked first. If it is not read ready, then the next listed mel is checked. Each mel is checked serially in the listed order until one is found read ready, or none is. If none of the mels are ready on the first pass, the task will do the OR parallel read and wait for the first mel that is read ready.

To re-emphasize, the serial checking is applicable only on the first pass. After that, the serial method behaves like the parallel method.

OR Read Methods Summary

Before moving on, let's summarize about the methods available for use in a mel OR read. The selected method is specified via the method attribute:
/* VERSION D */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try { Consume(<? timeout method=serial>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) { printf ("Wait too long"); }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
This table summarizes the values of the method attribute:

Mel OR Read Methods
MethodDescription
parallelThis is the default method if the method attribute is not specified. It gives all the listed mel elements a fair chance to be picked.
serialTo be in use, this method must be explicitly specified with the method attribute. This method establishes a hierarchy so that the first listed mel is the primary source, and the subsequent mels are backup sources. It is possible that none of the subsequent mels will be picked if the first listed mel is always read ready on the first pass.
customA NERWous implementation may support a custom method specific to a particular underlying platform. If a custom method is not recognized, NERWous C will fall back to the default method.

Since the values for the method behavior are reserved, their appearance in a mel OR read statement is sufficient without the use of the keyword method. We can simplify Version D as follows:
/* VERSION E */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try { Consume(<? timeout serial>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) { printf ("Wait too long"); }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
The examples for the mel OR reads make use of exception handling. This is now time to explore them in details.


OR Read Exceptions

Exceptions against mel OR reads are handled following these 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 read continues with the mel elements that have not had the exception. In other words, the program flow of the task still remains in the mel OR read operation.
     
  • Aborting rule: if there is a specific handler for that mel exception, the mel OR read 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 then processes the identified handler. In other words, the program flow of the task moves out of the mel OR read operation into the exception handler.
     
  • Escalation rule: if an exception happens to the last remaining mel element in a mel OR read, and there is no handler for that exception, the mel OR read is aborted with an unhandled exception. This exception escalates up the program flow until there is a handler. If no handler is found, the escalation will cause the task to abort (i.e. end abnormally).
     
  • Precedence rule: when an exception can be processed by multiple handlers, the more restrictive handler gets the work. Let's use the TIMEDOUT exception with two mel elements, store1 and store2, as an example:

    1. If there is one, a matching AND combination exception handler, such as:
      catch ( (store1 && store2)<TIMEDOUT> ) { ... }
      will be picked first. This is the most restrictive handler since all the mels in the AND list must have that exception.
       
    2. The next handlers are single mel handlers, such as:
      catch ( store1<TIMEDOUT> ) { ...}
      catch ( store2<TIMEDOUT> ) { ... } 
      If more than one single mel handlers match, CHAOS will arbitrarily pick one to proceed with the abortion rule.
       
    3. The last handlers are the OR combination exception handlers, such as:
      catch ( (store1 || store2)<TIMEDOUT> ) { ... }
      These are the least restrictive handlers since they match any of the listed mels, here either store1 or store2.

We will get to know more about these rules by looking at the TIMEDOUT and CLOSED exceptions during mel OR reads.

OR Read TIMEDOUT Exceptions

Common Timeout

When a task requests a mel OR read, it can include a timeout value, such as in:
Consume(< timeout>(store1 || store2))
Consume (< timeout=1000>(store1 || store2))
Only one single timeout can be specified, and this same timeout value applies to all the mel waits in the OR list. W hen the time is up, all the mel waits will trigger the TIMEDOUT exception. If there are many matching single mel exceptions, CHAOS will arbitrarily pick one handler to process, as stated by the Precedence rule.

Timeout Extension

Let's modify the Consumer example again:
/* VERSION F */
void Consumer (<mel> int store1, <mel> int store2) {
  int mypatience = 10; /* msec */
  while ( 1 ) {
    try { Consume(<? timeout=mypatience>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) {
       printf ("Increase my patience");
       mypatience += 5;
    }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
On the first round, Consumer is willing to wait for 10 msec for either store1 or store2 to be valued. If the timeout occurs before this can happen, Consumer jumps into the OR combination TIMEDOUT handler. It displays a printf message, and increases the amount it is willing to wait for another 5 msec. When it gets off the handler, Consumer iterates the while loop to retry the mel OR read, now with the expanded timeout value.

Timeout Combination Handler

In the above example, Consumer uses an OR combination handler to catch the TIMEDOUT exception of a mel OR read. It could also use an AND combination handler, as seen in the following example:
/* VERSION G */
void Consumer (<mel> int store1, <mel> int store2) {
  int mypatience = 10; /* msec */
  while ( 1 ) {
    try { Consume(<? timeout=mypatience>(store1 || store2)); }
    catch ( (store1 && store2)<TIMEDOUT> ) {
       printf ("Increase my patience");
       mypatience += 5;
    }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
An AND combination handler is entered only if all the listed mel elements have caught the exception. Due to the timeout being common, all mel waits time out at the same time, satisfying the AND condition. Thus, for the TIMEDOUT exception, the OR catch and the AND catch are equivalent. This may not be the case for other types of exceptions, such as the CLOSED exception we will discuss in the next section.

What happens if the Consumer task has both the OR combination and the AND combination TIMEDOUT handlers, like this version:
/* VERSION H */
void Consumer (<mel> int store1, <mel> int store2) {
  int mypatience = 10; /* msec */
  while ( 1 ) {
    try { Consume(<? timeout=mypatience>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) {
       printf ("Increase my patience a lot ");
       mypatience += 500;
    }
    catch ( (store1 && store2)<TIMEDOUT> )
       printf ("Increase my patience a little");
       mypatience += 5;
    }
    catch ( (store1 && store2)<CLOSED> ) { break; }
  }
}
Based on the Precedence rule, the AND combination handler takes precedence over the OR combination handler since it is more restrictive by nature. Thus, the timeout value is increased by 5 instead of 500 in the above example.

OR Read CLOSED Exceptions

Let's revisit our full Producer/Consumer example:
main () {
  <mel> int store1;
  <mel> int store2;
  <!> Producer (store1);
  <!> Producer (store2);
  <!> Consumer (store1, store2);
}
void Producer (<mel> int store) {
  while ( <?>store = Produce() );
  <close>store;
}

/* VERSION I */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    printf ("Start new iteration");
    try {  Consume(<? timeout>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) {
      printf ("Wait too long"};
      <resume>
    }
    catch ( (store1 && store2)<CLOSED> ) {
      break;
    }
  }
}
In the above example, we have two separate Producer tasks, one Produce'ing for store1, and the other for store2. We have one single Consumer task that Consumes either from store1 or store2, whatever is available at the moment. This Consumer has only one exception handler for the CLOSED exception:
catch ( (store1 && store2)<CLOSED> )
Independent Closeouts

Unlike the TIMEDOUT exception case where one mel wait timing out means that all the mel waits have also timed out, the CLOSED exception happens individually for each mel wait. One Producer task may have closed its associated "store" mel element, but the other Producer may still merrily Produce'ing for the other "store".

When CHAOS detects that a mel has closed, it looks for a matching CLOSED exception to process. In our example here, there is none since the specified AND combination handler requires both mels to be closed. In this case, CHAOS will apply the Skipping rule for mel OR reads. It removes the task from the waiting queue of the closed mel, but keeps the task waiting for the other active mel element(s). Since a mel OR read only needs one mel to satisfy the mel READ conditions, the task can stay with the mel OR read operation, instead of being aborted.

In our example, let's assume that store1 has been closed. The programmed AND combination handler is not matched since it requires that both store1 and store2 to be closed, and store2 is still operative. Eventually, store2 is valued and the Consumer task will Consume this value, and iterates the while loop. The mel OR read is again attempted. This time, CHAOS notices that store1 has been closed, so it will not put the Consumer task into store1 waiting queue. In this and subsequent iterations, the mel OR read on store1 and store2 is practically a single mel read on store2.

Specific Closeouts

With the Skipping rule, Consumer may not care if a mel in the OR list has closed. What if it does? Let's modify Consumer again:
/* VERSION J */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    printf ("Start new iteration");
    try {  Consume(<? timeout>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) {
      printf ("Wait too long"};
      <resume>;
    }
    catch ( store1<CLOSED> ) {
      printf ("[store1] has closed");
      <resume>;
    }
    catch ( store2<CLOSED> ) {
      printf ("[store2] has closed");
      continue;
    }
    catch ( (store1 && store2)<CLOSED> ) {
      break;
    }
  }
}
The new version of Consumer wants to do a printf of any mel in the OR list that has closed. It achieves this by having two single mel exception handlers, one for store1<CLOSED> and the other for store2<CLOSED>.

After processing the single exception handler, the Consumer task still wants to wait for a value from the other operative mel element so that it can eventually Consume that mel's value. The store1<CLOSED> handler uses the NERWous C <resume> operation to directly retry the mel OR wait operation, bypassing the while loop iteration. The store2<CLOSED> handler uses the C language continue statement which iterates the while loop, to allow it to printf another "Start new iteration" message.

In both single handler cases, the Aborting rule will cause Consumer to also abort the waiting on the other mel that has not closed, and the program flow goes back to the Consumer task. If there is no matching handler, then the Skipping rule takes precedence. The program flow stays with the CHAOS runtime, which simply skips the wait on any closed mels. Only when the last mel is closed, then CHAOS releases the program flow back to the task. Unless the task has a matching handler (such as an AND combination CLOSED handler), the exception will escalate per Escalation rule.

Combined Closeouts

An alternative to develop single CLOSED exception handlers for each mel element in the OR list, is to write an OR combination CLOSED handler. Let's modify Consumer again:
/* VERSION K */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    printf ("Start new iteration");
    try {  Consume(<? timeout>(store1 || store2)); }
    catch ( (store1 || store2)<TIMEDOUT> ) {
      printf ("Wait too long"};
      <resume>;
    }
    catch ( (store1 || store2)<CLOSED as=store> ) {
      printf ("[%s] has closed", store<name>);
      <resume>;
    }
    catch ( (store1 && store2)<CLOSED> ) {
      break;
    }
  }
}
The OR combination CLOSED handler makes use of the as attribute and the <name> property. When either store1 or store2 is closed, this handler is matched, and the stand-in store variable will be mapped to the mel variable of the closed mel. This allows the handler to use the <name> property to show the name of the closed mel.

On the first mel that has closed, the OR combination CLOSED handler is matched, and the handler wraps up with a <resume> operation to continue the wait on the other still operative mel element. If later on, this mel element is closed too, the AND combination CLOSED handler is then matched to break out of the infinite while loop.


Asynchronous Mel OR Reads

Like the single mel asynchronous reads, the combination mel OR reads can also be invoked asynchronously. Let's look at this code snippet:
<? async parallel as=store>(store1 || store2);
doSomething ();
Consume (<?>store);
The first thing to notice is that the mode and method keywords have been omitted, since their values can uniquely identify them. The full mel OR read statement is as follows:
<? mode=async method=parallel as=store>(store1 || store2);
When a mel OR read is invoked asynchronously, the as attribute with a stand-in value (here it is store) is required. This allows the task to use the stand-in mel variable to check for the outcome of outsourcing the mel OR wait to the CHAOS runtime.

In the code snippet above, the task invokes the mel OR read asynchronously, and while CHAOS does the mel OR wait for its stead, the task does a doSomething chore. Once the chore is done, it issues the read statement <?>store against the stand-in mel to check for the result of the mel OR read. If CHAOS has successfully done the read, the task will get the value from the local store<value> property. Because this is an OR read, this value can come from either the mel element store1 or store2. If CHAOS cannot do the read yet, then the task takes over the mel OR read of store1 and store2. Instead of re-issuing the full mel OR read list, the task uses the stand-in store mel variable.

Instead of taking back the mel OR wait too soon, the task may be able to conduct several rounds of doSomething by polling the wait:
<? async as=store>(store1 || store2);
while ( store<error> == NERW_ERROR_PENDING ) {
   doSomething ();
}
Consume (store<value>);
printf ("CHAOS gets the value from [%s]", store<name>);
In the new code snippet, the task after issuing the asynchronous mel OR read, keeps polling for the success of the read by checking the <error> property of the stand-in mel variable. On entry of a mel OR read, the <error> property is set to NERW_ERROR_PENDING.
Side note: Without the stand-in variable, the polling check is more verbose:
while ( (store1<error> == NERW_ERROR_PENDING ) && (store2<error> == NERW_ERROR_PENDING) )
The verbose statement is also error-prone, with an OR (||) clause wrongly used instead of the correct AND (&&).
If CHAOS is still working on the request, the task can do its doSomething chore. It checks back at the end of the chore, and keeps doing doSomething for each while loop iteration.

When CHAOS has done with the waiting in one mel, it changes the <error> property of the corresponding mel variable from NERW_ERROR_PENDING to NERW_ERROR_NONE if the read is successful, or to one of the NERW_ERROR_FAILED code to mark failure.

In the above snippet, the Consumer task assumes the asynchronous mel OR read is always successful; thus it expects a read value in the stand-in mel variable's <value> property. It can also know from which mel variable the read value comes from by checking the <name> property -- it is set to the string value "store1" or "store2".

Always assuming success is not robust code. Let's throw in some error checking in the code snippet:
<? async as=store>(store1 || store2);
while ( store<error> != NERW_ERROR_NONE ) {
   doSomething ();
   if ( store<error> & NERW_ERROR_FAILED ) break;
}
if ( store<error> == NERW_ERROR_NONE ) {
   Consume (store<value>);
   printf ("CHAOS gets the value from [%s]", store<name>);
}
else {
   printf ("Mel OR Read has failed on [%s] due to [%s]",
       store<error>, store<why>);
}
We can also use the try/catch exception method:
<? async as=store>(store1 || store2);
try {
   while ( store<error> != NERW_ERROR_NONE ) {
      doSomething ();
   }
   Consume (store<value>);
   printf ("CHAOS gets the value from [%s]", store<name>);
}
catch ( store<...> ) {
   printf ("Mel OR Read has failed on [%s] due to [%s]",
       store<error>, store<why>);
}
If there is an error during the asynchronous mel OR read, the exception is triggered when the task checks on the store<error>. It is not triggered during the doSomething chore.


Mel AND Reads

While the mel OR reads are for getting a value from one of the listed mel elements, the mel AND reads are for getting values from all of them. The AND mel read supports the following methods to process the listed mel elements:

Mel AND Read Methods
Method ValueDescription
CumulWith the cumulative method, the task checks all the mel elements in the AND list, and accumulates the read values as they become available. This method is efficient, and is the default method.
SimulWith the simultaneous method, the task checks all the mel elements in the AND list, wait for all of them to be available, then reads all of them at once. This method is more complex but does prevent deadlocks.
customA NERWous implementation may support a custom method specific to a particular underlying platform. If a custom method is not recognized, NERWous C will fall back to the default method.

To illustrate the mel AND read, let's first replace the Consume function which takes one argument, with the ConsumeTwo function which takes two arguments:
main () {
  <mel> int store1;
  <mel> int store2;
  <!> Producer (store1);
  <!> Producer (store2);
  <!> Consumer (store1, store2);
}
void Producer (<mel> int store) {
  while ( <?>store = Produce() );
  <close>store;
}

/* VERSION 1 */
void Consumer (<mel> int store1, <mel> int store2) {
  int c1, c2;
  while ( 1 ) {
    try {
      c1 = <? timeout>store1;
      c2 = <? timeout>store2;
      ConsumeTwo (c1, c2);
    }
    catch ( store1<TIMEDOUT> ) {
      printf ("Can't get store1 in time");
      <resume>;  /* retry store1 */
    }
    catch ( store2<TIMEDOUT> ) {
      printf ("Got store1 value of [%d]\n", store1<value>);
      printf ("Can't get store2 in time");
      <resume>;  /* retry store2 */
    }
    catch ( (store1 || store2)<CLOSED> ) {
      break;  /* stop on first closure */
    }
  }
}
The Consumer task first waits for store1. Once it gets the value from store1, it waits for store2. Once it gets both values, it will invoke the serial function ConsumeTwo with the two values as input arguments.

We now make Consumer more compact by using the mel AND read construct:
/* VERSION 2 */
void Consumer (<mel> int store1, <mel> int store2) {
  int c1, c2;
  while ( 1 ) {
    try {
      /* The mel AND wait returns two values to initialize c1 and c2 */
      (c1, c2) = <? timeout as=store>(store1 && store2);
      ConsumeTwo (c1, c2);
    }
    catch ( store<TIMEDOUT> ) {
      printf ("Can't get %s in time", store<name>);
      <resume>;  /* retry store1 or store2 or both */
    }
    catch ( store<CLOSED> ) {
      printf ("%s has closed", store<name>);
      break;  /* stop on first closure */
    }
  }
}
Comparing the two versions of Consumer, the AND mel read version offers the benefit that all listed mels are waited in parallel, instead of serially one after the other, thus potentially cutting down on the overall wait time.
Side note: A computer linguist will note that the C language does not support the return of multiple values. A C function returns at most one value. Let's say that NERWous C has taken the liberty to expand the language with the support of multiple returns from a function. The multiple returns are enclosed within the ( ) markers. When checking for true and false, if all the returned values are true then the multiple return is true. If any returned value is false, then the multiple return is false.
Notice the use of the stand-in mel variable, store. This allows the condition for a handler exception to be written more succinctly. For example, instead of catch (store1 || store2)<TIMEDOUT>, we have catch (store<TIMEDOUT>). The identity of the mel that causes the exception can be found via the stand-in mel variable properties, <name> and <id>.

The version above can be further shortened:
/* VERSION 3 */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      ConsumeTwo (<? timeout as=store>(store1 && store2));
    }
    catch ( store<TIMEDOUT> ) {
      printf ("Can't get %s in time", store<name>);
      <resume>;  /* retry store1 or store2 or both */
    }
    catch ( store<CLOSED> ) {
      printf ("%s has closed", store<name>);
      break;  /* stop on first closure */
    }
  }
}
Without an explicit method, the AND mel wait uses the default cumulative method. The following statements are equivalent:
<?>(store1 && store2);
<? cumul>(store1 && store2);
<? method=cumul>(store1 && store2);
If the simultaneous method is desired, it has to be explicitly specified. The following statements are equivalent:
<? simul>(store1 && store2);
<? method=simul>(store1 && store2);
Since cumul and simul are unique keywords, NERWous C can associate them with the attribute method, which can then be dropped.


Cumulative AND Reads

Let's look at the behind-the-scene process of the cumulative method of a mel AND read with a timeout specified, such as:
<? timeout>(store1 && store2);
  1. The task puts itself to the default readers' queues of all the listed mels, with the specified timeout value. All the corresponding mel variables have their <error> property reset to NERW_ERROR_PENDING, per On Read Entry setting.
     
  2. If the mel READ conditions are met for one of the mel elements, CHAOS will do the read operation on that element. The top value of the mel buffer is removed and deposited into the <value> property of the corresponding mel variable. Its <error> property is changed to NERW_ERROR_NONE to denote that the read has been successful. The task takes itself off the wait queue of that mel element.
     
  3. The task collects available read values as they happen, and continues to wait at the other mels' queues until those mels also meet the mel READ conditions.
     
  4. Once the task gets a value of all the mels in the AND list, it gets off the AND mel read, and resumes the program's code flow. All the corresponding mel variables have their <value> property updated and their <error> property set to NERW_ERROR_NONE to mark all successful reads.
     
  5. The AND mel read can also ends with a failure, with the task not receiving all the values from the requested mels in the AND list. The first failure from any mel will cause the AND mel read to fail. For a cumulative method, when the task gets off an AND mel read with a failure, the mel variables of the listed mels have the following properties settings:

    Successful reads
    Mels that have been successfully read have their corresponding mel variables with <value> property updated and <error> property changed to NERW_ERROR_NONE.
    Failed reads
    The mel that causes the failure has its corresponding mel variable with <value> property unchanged from entry, and <error> property set to the error code of the failure.
    Un-filled reads
    Mels that are still waiting to be read have their corresponding mel variables with <value> property unchanged from entry, and <error> property remaining at NERW_ERROR_PENDING.
Let's explore how a task handles some of the failures with the cumulative method:

Cumulative AND Reads Timeouts

Like all mel reads, timeouts are optional for mel AND reads. The task has to request it by specifying the timeout attribute in the mel AND read, such as:
<? timeout=10>(store1 && store2);
These are some notes about timeouts used with cumulative AND reads:
  1. A single timeout is applied to all the mel waits.
     
  2. Any mels that have been read before the timeout occurs, will be removed from the timeout consideration. Their corresponding mel variables have the <value> property updated and the <error> property changed to NERW_ERROR_NONE.
     
  3. When the timeout occurs, all remaining mel waits will time out, due to the single common timeout attribute. Their corresponding mel variables have the <value> property unchanged and the <error> property set to NERW_ERROR_TIMEDOUT.
     
  4. When a task gets off an AND mel read due to a timeout exception, the <error> property of the mel variables are either NERW_ERROR_NONE if a read has been done, or NERW_ERROR_TIMEDOUT if a read has not happened yet. There will be no mel variable with the <error> setting of NERW_ERROR_PENDING.
     
  5. When a timeout occurs, the task gets off all the remaining mel waits, and CHAOS raises a TIMEDOUT exception against the task. If the mel AND read has a matching handler, the programming flow will jump to the handler code. Otherwise, the exception is escalated to a higher exception handler of the task. If there is none, or none matching, the task is aborted and a pel exception is generated.
     
  6. CHAOS raises a TIMEDOUT exception to all the remaining mel waits. However since a program does not know which mels have been read and which ones not, the recommended TIMEDOUT handler for a cumulative mel AND read is the OR combination handler, such as:
    catch ( (store1 || store2)<TIMEDOUT> ) {  }
    If a stand-in mel variable is specified as an attribute to mel AND wait, the OR combination handler can be abbreviated to:
    catch ( store<TIMEDOUT> ) { }
  7. If the logic of the program demands, single TIMEDOUT handlers can be added. For example:
    catch ( store1<TIMEDOUT> ) {
        printf ("Store 1 has timed out");
        alertTheProducerOfStore1ToSpeedUp ();
        <resume>;
    }
    
    The Precedence rule also applies to cumulative AND reads. When a timeout occurs, CHAOS needs to pick a handler to process that exception. If there are multiple matching handlers, CHAOS will pick the more restrictive one. Thus, if there are both matching single handlers and an OR combination handler, CHAOS will pick the single handler. If there are multiple single handlers that match, CHAOS will arbitrarily pick one.
     
  8. It is not recommended to have an AND combination handler for cumulative AND reads for TIMEDOUT exceptions, such as:
    catch ( (store1 && store2)<TIMEDOUT> ) {
        printf ("All the mel waits time out at the same time");
        <resume>;
    }
    
    That handler does not work if either store1 or store2 has been successfully read already. It can only work if neither has been read. However, in this case, the OR handler is still better since it also works with the "either" case.
Let's modify the Consumer task to see how it gets off a cumulative AND read TIMEDOUT exception handler:
/* VERSION 4 */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      ConsumeTwo (<? timeout as=store>(store1 && store2));
    }
    catch ( store1<TIMEDOUT> ) {
      printf ("[Store1] has timed out");
      informProducerToSpeedUp (1);
      <resume>;  /* retry store1 or both */
    }
    catch ( store<TIMEDOUT> ) {
      printf ("Can't get %s in time", store<name>);
      <resume>;  /* retry store1 or store2 or both */
    }
    catch ( store<CLOSED> ) {
     printf ("[%s] has closed", store<name>);
     break;  /* stop on first closure */
    }
  }
}
Let's run through some scenarios.
store1 times out
The Consumer task gets off all the queues it is still waiting on, and jumps into the single exception handler catch ( store1<TIMEDOUT> ). The reason for this handler is that the task can ask the Producer of store1 to speed up. The handler then invokes the <resume> operation.

The <resume> operation does a fresh cumulative mel AND read, without iterating through the while loop. For store1 that times out, the Consumer task puts itself back into its waiting queue. For store2, it is a little more complicated. If store2 has been successfully read, then Consumer does not wait for it anymore. Otherwise, Consumer also puts itself to the bottom start of store2 waiting queue.
 
store2 times out
The Consumer task gets off all the queues it is still waiting on, and jumps into the OR combination exception handler represented by the stand-in mel variable store:
catch ( store<TIMEDOUT> )
After printing the store2 information, the handler invokes the <resume> operation.

The <resume> operation redoes the cumulative mel AND read. For store2 that times out, the Consumer task puts itself back into the waiting queue. For store1, Consumer redoes the mel wait only if store1 has not been successfully read.
In both scenarios, if instead of <resume>, the handler were to use continue to iterate the while loop, then the mel AND read would also be invoked again, but in this new invocation, the Consumer task would be automatically put into both mels waiting queues. If during the previous invocation (which has timed out), a value had been successfully read, this value would be irrevocably lost. On the other hand, the <resume> operation intends to make good on the existing cumulative mel AND read. Accumulated values are retained, and only un-filled values will be waited again.

Cumulative AND Reads Closeouts

Another cause of failure is that one mel is closed. Since any mel in the AND list can be closed at any time, the recommended handler is the OR combination, and the get-off strategy is a break. For example:
catch ( (store1 || store2)<CLOSED>) ) {
    break;  /* stop on first closure */
}
In the above example, the Consumer task also wants to know which mel element has timed out. This is resolved by using the stand-in mel variable store:
catch ( store<CLOSED> ) {
    printf ("[%s] has closed", store<name>);
    break;  /* stop on first closure */
}

Let's consider some other scenarios:
Single CLOSED exception handler
Per Precedence rule, if a single handler exists and matches, it will be processed over the recommended OR combination handler:
catch ( store1<CLOSED> ) {
    printf ("[store1] has closed");
    break;
}
The above handler only matches the store1 closeout. In our example, if store2 is closed, it will be handled by the OR combination handler.
 
AND combination CLOSED exception handler
This handler:
catch ( (store1 && store2)<CLOSED> )
is more problematic than it is usually worth. Normally this handler will not get matched since both mels must be closed at the same time, which rarely happens if ever, because mel closures are usually independent from one another, and not synchronized.

One contrived scenario that can cause the AND combination CLOSED handler to ever get matched, is when there is a single handler that gets off with a <resume> operation. The AND mel wait is resumed, CHAOS detects that one mel has closed, the single handler is re-matched, the program flow jumps back into that single handler again, which again does a <resume> back into the mel AND read; this livelock spin prevents the task to do other work, and continues until the rest of the mels get closed by other task or tasks. Only at that time, due to the precedence rule, the AND combination CLOSED handler, which is more restrictive than any single handler, will get picked.
 
No resumption on closeouts
An AND mel read is successful if all the listed mels are successfully read. If one fails due to closure, the AND mel read will definitely fail. Like all exceptions, a CLOSED exception is cleared on exit of the serving handler. However while a timeout period is restarted on re-entry of a mel read, a mel closure condition remains and is detected immediately again, causing the CLOSED handler to be re-entered, resulting in a livelock spin.

If the AND mel read is inside an infinite loop, it is also not recommended to use the continue get-off strategy. Let's look at this example:
/* VERSION 5 */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      ConsumeTwo (<?>(store1 && store2));
    }
    catch ( (store1 || store2)<CLOSED>) ) {
      continue;
    }
  }
}
The continue statement will loop back the while loop, and a new mel AND read is attempted again. CHAOS will again see that one of the mels (or both) has closed, and transfer the program flow of the task to the CLOSED exception handler again, which again loops back to the while loop. Again, we have a spinning livelock.

The only viable get-off strategy in case of a mel closure during an AND mel read is to get out of the read, and do not attempt to retry it via resumption or iteration.
 
Orphan reads on closeouts
A mel AND read has many mels listed. Some of these mels may have been successfully read before one of the remaining mels gets closed, which usually triggers the task to get off the mel AND read. What happens then to the values that have been read?

This is up to the task what to do with these values. Let's explore the scenarios:
  • Mels with successful reads:
    Mels that have been successfully read have their corresponding mel variables with <value> property updated and <error> property changed to NERW_ERROR_NONE.
  • Closed mels:
    Mels that are closed, have their corresponding mel variables with <value> property unchanged and <error> property set NERW_ERROR_CLOSED.
  • Mels still in waiting:
    Mels that are still waiting to be read have their corresponding mel variables with <value> property unchanged and <error> property remaining at NERW_ERROR_PENDING.

The following example shows a Consumer that does not want to lose any read values:
/* VERSION 6 */
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      ConsumeTwo (<? as=store>(store1 && store2));
    }
    catch ( store<CLOSED>) ) {
      if ( store1<error> == NERW_ERROR_NONE )
        Consume (store1<value>);
      if ( store2<error> == NERW_ERROR_NONE )
        Consume (store2<value>);

      break;
    }
  }
}
If the OR combination CLOSED handler is entered because of either store1 or store2 closing, it checks if either mel variables have been successfully read, and if so, it will Consume the read value, before breaking off the while loop.


Simultaneous AND Reads

The simultaneous method is a very special case of mel AND reads. It is used when there is a potential for circular deadlocks. To resolve such possibilities, it adds another layer between the mel readers' queues and the mel variables of the requesting tasks.

Let's look at the following example:
<mel> int store;
main () {
  <mel> int leftkey = 1;
  <mel> int rightkey = 1;

  <!>Producer ();
  <!>ConsumerRH (rightkey, leftkey);
  <!>ConsumerLH (leftkey, rightkey);
}
void Producer () {
  while ( <?>store = Produce() );
  <close>store;
}
void ConsumerRH (<mel> rightkey, <mel>leftkey) {
  while ( 1 ) {
    try {
      <? method=simul>(rightkey && leftkey);
      ConsumeRH (<?>store ) {
      <?>rightkey = 1;
      <?>leftkey = 1;
      }
    }
    catch ( store<CLOSED> ) {
      break;
    }
  }
}
void ConsumerLH (<mel>leftkey, <mel> rightkey) {
  while ( 1 ) {
    try {
      <? simul>(leftkey && rightkey);
      ConsumeLH (<?>store ) {
      <?>leftkey = 1;
      <?>rightkey = 1;
      }
    }
    catch ( store<CLOSED> ) {
      break;
    }
  }
}
Both "Consumer" tasks issue a mel AND read for the "right and left keys". The ConsumerRH task uses the keyword method to introduce the simultaneous method. As said previously, this keyword can be omitted since its values are well-known. The ConsumeLH task takes advantage of this permission, and just uses the value simul.

If the simultaneous method is not specified, the mel AND reads default to the cumulative method, and this situation can happen:
  1. The main task initializes the leftkey and rigthkey
  2. The ConsumerRH task grasps the rightkey value, causing the rightkey mel to become empty.
  3. The ConsumerLH task grasps the leftkey value, causing the leftkey mel to become empty.
  4. When ConsumerRH tries to grasp the leftkey, it finds leftkey to be empty.
  5. When ConsumerLH tries to grasp the rightkey, it finds rightkey to be empty.
  6. The mel AND reads at ConsumerRH and ConsumerLH are stuck in a circular deadlock./li>
The use of the simultaneous method prevents this circular deadlock to happen, or resolves it if it has already happened. The simultaneous method has only one requirement - the all-or-nothing requirement. When a simultaneous mel AND read ends, either it ends successfully with all the mel variables valued, or it ends unsuccessfully with none of the mel variables getting valued. In contrast, a cumulative mel AND read can end with a partial success, with some mel variables valued, and some not.

NERWous C does not define an implementation for the simultaneous method for mel AND reads. The computer literature contains a great number of discussions on resolving circular deadlocks due to competition of shared resources. These discussions usually relate to the Dining Philosophers problem. Depending on the physical and runtime environment, a particular algorithm may be a better fit than another; thus, the implementation is left to the designers of an actual NERWous C compilation or translation tool set.

In the above example, suppose that the ConsumerRH task already gets hold to the rightkey mel value, and is waiting for the leftkey value. If this value is given to the task ConsumerLH then a deadlock will result, with each task holding a key and waiting for the other key, and no ex-machina producer task to generate new values for those keys. An implementation of the simultaneous method either prevents the deadlock to happen in the first place (e.g., by giving the leftkey to ConsumerRH to have one less mel AND wait cases pending), or resolves the deadlock after it happens (e.g., by yanking the rightkey from ConsumerRH and giving it to ConsumerLH).

Simultaneous AND Reads Timeouts

The above example has the mel AND reads without timeouts. Let's now introduce this facility for the ConsumerLH task:
void ConsumerLH (<mel>leftkey, <mel> rightkey) {
  while ( 1 ) {
    try {
      <? simul timeout as=key>(leftkey && rightkey);
      ConsumeLH (<?>store ) {
      <?>leftkey = 1;
      <?>rightkey = 1;
      }
    }
    catch ( key<TIMEDOUT> ) {
      printf ("Got a timeout after [%d]\n", key<timeout> );
      <resume timeout+10>;
    }
    catch ( store<CLOSED> ) {
      break;
    }
  }
}
With this change, the task ConsumerLH wants to have a checkpoint TIMEDOUT exception after the default timeout is reached. The exception entry key<TIMEDOUT> uses the stand-in mel variable key, and is the short-cut for (rightkey || leftkey)<TIMEDOUT>. After the checkpoint printf, the ConsumerLH task increases the current timeout period by 10 msec, and <resume>s the mel AND wait at the starting positions in both mel queues.

There are similarities between timeouts for cumulative and simultaneous mel AND reads:
  • Timeouts are optional.
    If no timeout is specified, a simultaneous AND read will wait forever until all the listed mel elements are successfully read, or until one mel read fails due to other failures other than timeouts.
     
  • The single timeout is applicable to all.
    All mel waits use the same timeout value specified in the simultaneous mel AND read. This is true on the original timeout and also on updated timeouts. For example, the ConsumerLH task, in its TIMEDOUT exception handler, increases the amount of timeout by 10 msec for a subsequent resumption:
    <resume timeout+10>;
    This new timeout value will be applicable to all mels in the simultaneous mel AND wait.
     
There are differences between timeouts for cumulative and simultaneous mel AND reads:
  • All timeouts happen at the same time.
    The simultaneous mel AND read does not collect mel values one at a time as they meet the mel READ conditions. The implemented AND read underlying algorithm may collect the mel values that way, but it only gives these values to a waiting task if all the mel values have been assembled. In other words, it is all or nothing. (If there are tasks that compete for circular shared resources, the task that receives all the waited mel values is the one that the algorithm decides that will prevent a deadlock to occur, or the one that resolves an occurring deadlock.)

    Since a task cannot see a mel value until it sees all of them, a timeout that occurs in one mel wait, is applicable to all mels in the AND list. In the example above, ConsumerRH catches the exception with an OR combination handler:
    catch ( (rightkey || leftkey)<TIMEDOUT> )
    which is abbreviated to this statement due to the availability of the stand-in mel variable key:
    catch ( key<TIMEDOUT> )
    The ConsumerLH can also use the more restrictive AND combination handler:
    catch ( (rightkey && leftkey)<TIMEDOUT> )
    Due to the all-or-nothing nature of the simultaneous method, both the OR or AND combination handlers will properly catch the TIMEDOUT exception.
     
  • Available values still time out.
    A peculiarity of the simultaneous mel AND read is that a mel that satisfies the mel READ conditions of a task, can still cause that task to time out. This is because the mel value is read by the implemented AND read algorithm, and this algorithm has decided to not give that value to the task due the potential of a deadlock. The algorithm may be waiting for other mel values to be available and give all of them to the task in one shot.

    If a timed-out task redoes the simultaneous mel AND read, either via a resumption or an iteration, it may have the chance to get the mel value that is not granted to it the first time. If the value is still available, the algorithm may now find the conditions good enough for the task to receive that value and the rest.
     
  • Resumption is for all mel queues.
    With the cumulative mel AND read, a <resume> operation from a TIMEDOUT exception handler puts the task back into the queues of mel elements that the task has not accumulated the values yet. For the simultaneous mel AND read, its all-or-none nature has the task being put back into all the queues of requested mels.

    When the task is put back into a queue, it begins the wait at the bottom of the queue (i.e. the starting position). It has to move up to the top position of the queue, to satisfy one of the conditions of the mel READ conditions, before it can be considered by the implemented AND read algorithm. If there are other tasks in the queue waiting on other mel reads, the task will have to wait for its due time.
     
  • Available mel values may not be read
    With the cumulative mel AND read, once a value shows up on the mel buffer, it will likely be read by one the requesting tasks that has met the mel READ conditions. For the simultaneous mel AND read, the implemented AND read algorithm will take over the reading. It may read out the value into some internal holding area. This will allow other tasks in the mel queue to get to subsequent values in the mel buffer. If the algorithm does not support a holding area, it may leave the task at the top of the queue to hold up the mel buffer value. In this case, any queued tasks have to wait for the algorithm to resolve the no-deadlock AND wait problem before they can access the mel buffer.
While the simultaneous mel AND read looks easy to use from the coder level -- just add the method keyword simul, things get more complicated at the runtime level. The implemented AND read algorithm may require additional resources, and in general will cause mel waits to be lengthened. The simultaneous mel AND read should not be used unless warranted.

Simultaneous AND Read Closeouts

While the simultaneous AND wait is going on, one of the waited-on mel elements may get closed by some other task. Like for the closeouts for cumulative AND waits, the recommended handler for the <CLOSED> exception is the OR combination:
catch ( (rightkey || leftkey)<CLOSED> )
To prevent livelocks, the handler should not get off with a resumption or iteration attempt, since any subsequent attempt to do a simultaneous mel AND read that includes a mel that has been closed, will result in another <CLOSED> exception.

While there are many similarities between a cumulative and simultaneous mel AND read, there are many differences between them too.
No accumulated values
A task doing a cumulative mel AND wait, may be able to read out some values for its mel variables from the mel request list before it gets a <CLOSED> exception handler. If it does a simultaneous mel AND read, the all-or-nothing nature of the read insures that the task will have none of its mel variables valued. Even if there are values to be had, the implemented AND read algorithm still holds them back.
 
Lost accumulated values
The implemented AND read algorithm holds back the generated mel values until it can give to a task all the values it requests in the mel AND list. If a mel gets closed, the task will abort the mel AND read with the <CLOSED> exception, getting none of the accumulated values. What happens to these accumulated values then? They are simply available to other tasks that also do simultaneous AND read. If the implemented AND read algorithm cannot find any task to give the accumulated values, they will be discarded at the end of the NERWous C program.

When the <CLOSED> exception happens to a task doing a cumulative mel AND read, it gets granted the accumulated mel values, and in the <CLOSED> exception handler, it can decide to discard these values or make some remedial use. With the accumulated values being held by the implemented simultaneous AND read algorithm, there is no such option.



Asynchronous Mel AND Reads

We have seen that the asynchronous read mode is applicable to mel OR reads. It is also applicable to mel AND reads, for both the cumulative and simultaneous methods. Let's take a look at this Consumer example:
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    <? mode=async>(store1 && store2);
    doSomething ();
    ConsumeTwo (<?>store1, <?>store2);
  }
}
The Consumer above needs the values of the mel elements store1 and store2 for its ConsumeTwo chore. Instead of immediately waiting for them, it outsources the AND mel wait to the CHAOS runtime by issuing an early asynchronous mel AND read request:
<? mode=async>(store1 && store2);
While CHAOS stands in line in the mel queues, Consumer uses that time to doSomething. Once done, it issues the mel read requests on the mel elements. Although the mel store1 is processed first in this statement
ConsumeTwo (<?>store1, <?>store2);
the mel AND read will not complete until all the listed mel values are available. In other words, due to the default cumulative method used, CHAOS can accumulate the value of store1, but it will not give it to the Consumer task until the value of store2 is also accumulated.

As in the asynchronous read of single mels, the <?>store1 and <?>store2 requests mean that the Consumer task will use the <value> properties of the local mel variables if CHAOS has successfully carried out the mel AND wait; otherwise Consumer will relieve CHAOS and take over the wait.
Side note: As a reminder, the asynchronous mel AND wait is most useful if there are many readers vying for the same mel elements. This allows a task to put itself in the queue as early as possible to "stake its position". If the task is the only consumer of the mel resources, there is no need for a "pay someone to stand in line for me" scheme. In our Producer/Consumer example, since Consumer is the only reader task, Consumer can actually be written as simply as follows:
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    doSomething ();
    ConsumeTwo (<?>store1, <?>store2);
  }
}
We are making things more complicated than necessary here for the sake of discussion.

The default AND read method is the cumulative method. If we want the Consumer task to use the simultaneous method, we have to specify it:
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    <? mode=async method=simul>(store1 && store2);
    doSomething ();
    ConsumeTwo (<?>store1, <?>store2);
  }
}
We can omit the attribute keywords, mode and method:
<? async simul>(store1 && store2);

Error Handling For Asynchronous Mel AND Reads

Let's modify Consumer to add some error handling:
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    <? async timeout as=store>(store1 && store2);

    while ( store<error> == NERW_ERROR_PENDING )
      doSomething ();

    switch ( store<error> ) {
      case NERW_ERROR_NONE:  /* success */
        ConsumeTwo (store1<value>, store2<value>);
        continue;  /* while(1) loop */

      case NERW_ERROR_TIMEDOUT:
        printf ("[%s] has timed out - TAKE OVER READ\n", store<name>);
        ConsumeTwo (<?>(store1 && store2));  /* take over the wait */
        continue;  /* while(1) loop */

      case NERW_ERROR_CLOSED:
         printf ("[%s] has closed - STOP READ\n", store<name>);

         /* fall through the default case */
      default:
         printf ("[%s] fails on [%d] due to [%s] - STOP READ\n",
           store<name>, store<error>, store<why> );

         if ( store1<error> == NERW_ERROR_NONE )
           Consume (store1<value>);
         if ( store2<error> == NERW_ERROR_NONE )
           Consume (store2<value>);

         break;  /* out of while(1) loop */
    }
  }
}
The above Consumer task first requests an asynchronous mel AND read with timeout. While the CHAOS runtime does the waiting, the task runs the doSomething chore. It keeps doing the doSomething until it detects that CHAOS has ended the mel AND read. The ending may be a success or failure. If it is a success, the task runs ConsumeTwo to process the read values of the mel stores, and iterates the while loop to repeat the same process. If the asynchronous mel AND read times out, the Consumer task, not having anything better to do, takes over the mel AND read from CHAOS. Once it gets the mel values, Consumer runs ConsumerTwo, then iterates the while loop to repeat the process. For other kinds of failures, the Consumer task gets off the while loop. But before doing that, since the mel AND read is a cumulative one, one of the two mel reads may have been successful. Instead of forfeiting this mel value, Consumer salvages it with the single-parameter Consume function.

The Consumer task achieves the above by using the <error> mel variable. First, it sets the store mel variable as the stand-in for the mel AND read of store1 and store2. The invoked asynchronous mel AND read sets the <error> property of both mel variables to NERW_ERROR_PENDING. While the <error> variables have this value, CHAOS is still running the mel AND read on behalf of Consumer. This allows Consumer to run doSomething. The check in the while loop:
store<error> == NERW_ERROR_PENDING
uses the stand-in variable store as a short-cut way for:
store1<error> == NERW_ERROR_PENDING || store2<error> == NERW_ERROR_PENDING
Since the mel AND read is invoked with the default cumulative method, the variables can have their <error> values changed at separate times. When both values have been changed, the Consumer task knows that CHAOS is done with the mel AND read. To know if this read is successful or not, Consumer again makes use of the <error> property. The construct:
switch ( store<error> ) {
  case NERW_ERROR_NONE: ...
  case NERW_ERROR_TIMEDOUT: ...
  case NERW_ERROR_CLOSED: ...
  default: ...
}
is a short-cut for:
int _err1 = store1<error>, _err2 = store2<error>;
if ( _err1 == NERW_ERROR_NONE && _err2 == NERW_ERROR_NONE ) { ... }
else if ( _err1 == NERW_ERROR_TIMEDOUT || _err2 == NERW_ERROR_TIMEDOUT ) { ... }
else if ( _err1 == NERW_ERROR_CLOSED || _err2 == NERW_ERROR_CLOSED ) { ... }
else { ... }
For a successful read, both <error> values must be NERW_ERROR_NONE. For a failed read, only one <error> variable needs to have a failed error code. Using the stand-in store mel variable ensures that the AND (&&) and OR (||) operators are applied correctly, besides resulting in a more concise code.

For a successful read, the local <value> properties contain the read values, and Consumer uses them directly for the ConsumeTwo chore. For generality, Consumer can also use the mel AND wait construct:
ConsumeTwo (<?>(store1 && store2));
The above wait construct first checks the local mel <value> properties. If fresh values are found there, the wait construct will return them immediately, instead of going out to the remote mels and doing mel waits. In the case above, we are sure to find fresh values, so it is more efficient to use the <value> properties directly.

For the timeout failure, if the method used for mel AND read were the simultaneous method, its all-or-nothing would have both mel variables timed out. Since the cumulative method is in use here, one mel variable may already be valued (with its <error> property set to NERW_ERROR_NONE), and the other mel variable timed out. The task Consumer knows which mel variable whose timeout causes the NERW_TIMEDOUT_ERROR case, by checking the <name> property of the stand-in variable store.

Once a timeout has been detected, Consumer chooses to take over the wait from CHAOS. It issues a mel AND wait, and will be willing to wait until both of the requested mels are valued. Note that if one mel variable is already valued (by the CHAOS runtime from the asynchronous mel AND read), the new mel AND read will wait only on the other mel.
Side note: An astute programmer will notice that there is no error checking for this second mel AND read. This statement:
ConsumeTwo (<?>(store1 && store2));
is a concise way to write:
<?>(store1 && store2);
if ( store1<error> == NERW_ERROR_NONE && store2<error> == NERW_ERROR_NONE )
    ConsumeTwo (store1<value>, store2<value>);
What happens if the <error> properties are not NERW_ERROR_NONE? If this is the case, the mel AND read has failed, the ConsumeTwo function will not be invoked. The Consumer task gets off the case NERW_ERROR_TIMEDOUT with a continue to iterate the loop. The asynchronous mel AND read is requested again; and this time, any mel deficiency discovered from the previous iteration, will be handled by this iteration's switch/case statements.
For the CLOSED and other error conditions, the Consumer task breaks off the while loop. Before doing that, the task tries to salvage any values that have been successfully read during the asynchronous attempt. Those values are in the <value> properties of the mel variables whose <error> properties have been changed from NERW_ERROR_PENDING to NERW_ERROR_NONE.
Side note: This salvage work would not make sense if the AND read method were simultaneous. With its all-or-none nature, there would not be any successful partial reads on failure.

Exception Handling For Asynchronous Mel AND Reads

An alternative to using the <error> property for checking for mel AND read results is to work with exceptions. Let's modify the Consumer code again:
void Consumer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      while ( <? async simul timeout as=store>(store1 && store2) )
        doSomething ();
      ConsumeTwo (<? tag=TAKEOVER as=store>(store1 && store2));
    }
    catch ( store<TIMEDOUT> ) {
      printf ("[%s] has timed out - TAKEOVER READ\n", store<name>);
      <resume tag=TAKEOVER>;  /* take over the wait */
    }
    catch ( store<...> ) {
      if ( store<error> == NERW_ERROR_CLOSED )
        printf ("[%s] has closed - STOP READ\n", store<name>);
      else
        printf ("[%s] fails on [%d] due to [%s ] - STOP READ\n",
          store<name>, store<error>, store<why>);

      break;  /* stop on first failure */
    }
  }
This time, we use the simultaneous method with the keyword simul. The statement:
while ( <? async simul timeout as=store>(store1 && store2) ) doSomething;
is a concise way to write:
<? async  simul timeout as=store>(store1 && store2);
while ( store<error> == NERW_ERROR_PENDING ) doSomething;
The statement
ConsumeTwo (<? tag=TAKEOVER as=store>(store1 && store2));
can be entered two ways. The first way is when the asynchronous mel AND read is successful, and the arguments that ConsumeTwo takes in are in fact store1<value> and store2<value>. There is no waiting involved even though the mel wait ? is present.

The second way that the ConsumeTwo statement is entered is via the <resume tag=TAKEOVER> statement invoked inside the store<TIMEDOUT> exception handler. The Consumer task now wants to take over the mel AND read from the CHAOS runtime. By default, the <resume> operation picks up on the last mel operation, which is the asynchronous mel read statement:
<? async simul timeout as=store>(store1 && store2)
which generates the timeout exception. By specifying the tag attribute in <resume>, the Consumer task requests the mel AND wait to be resumed at the ConsumeTwo statement which has the same tag attribute. (Without using tag, the <resume> operation would redo the asynchronous mel AND wait.)

In the catch-all store<...> handler, Consumer makes the wise decision to break out of the infinite while loop since future iterations will likely fail on the AND mel read. Before the breakout, Consumer can use the stand-in store variable to display information about the failure. It is not in the code above, but if Consumer wants to do something special with store1, it can via the if statement:
if ( store<name> == "store1" ) { ... }


Previous Next Top

Sunday, December 23, 2018

Mel Global Variables

Welcome » NERWous C » Mel
  1. Global Variable Scope
  2. Global Variable Usage
    1. Semantic Sharing
    2. Code Sharing
    3. Name Discovery
    4. No Global Variables
  3. Global Variable Creation
    1. Multiple Global Variables
    2. No Exception Handling
    3. No Extern Statement
  4. Global Variable Extern Statement
    1. Extern Statement Signature
    2. Extern Statement Translated
    3. Extern Statement Chain
    4. Extern Statement Short Passing
    5. Extern Statement Multiple Values
    6. Extern Statement Macro
  5. Global Variable Closure


Global Variable Scope

In the Mel Basic chapter, we have touched on the topic of mel scope. Let's revisit the example used:
<mel> int GlobalStore;    /* global variable */
main () {
     <mel> int localStore;     /* local variable */
     <!> Producer (localStore);
     <!> Consumer ();
}
void Producer (<mel> int store) {
    <extern> GlobalStore;
    <?>GlobalStore = Produce();   /* OK - GlobalStore is global */
    <?>store = Produce();         /* OK - store is local */
    <?>localStore = Produce();    /* ERROR - localStore is not in scope */
}
void Consumer () {
    Consume (<?>GlobalStore);     /* ERROR - GlobalStore is not in scope */
    Consume (<?>store);           /* ERROR - store is not defined */
    Consume (<?>localStore);      /* ERROR - localStore is not in scope */
}
Mel variables, like GlobalStore and localStore above, appear in NERWous C programs based on their scope rules: either local or global. The enforcement of the scope rules is during compile time. The programmer has to resolve the ERRORs flagged in the above program before it can be compiled for execution.

We have seen local mel variables in use extensively in the previous chapter on Mel Operations. In this chapter, we focus on mel variables as global variables.

Note that the remote mel elements are always "global", in the sense they can be accessed at run-time anywhere in a NERWous C program. The scope rules apply only at compile time and only to the mel variables, and not the mel elements. When mel variables have local scope, they appear only inside a function or block of code. When they have global scope, they can appear anywhere in the program, but only with the help of the <extern> statement. Incorrect appearances will trigger errors during compilation.

The CORE properties of a mel variable allow a task to reach out to a mel element. For local mel variables, the CORE properties are passed by value from the parent task to the children tasks via function arguments. For a global mel variable, the CORE properties can also be passed from the parent task to the children tasks, but only if the parent task knows about this particular global mel variable. If it does not, the children tasks have to query the CHAOS runtime for the CORE properties to initialize their own global mel variables to reach out to the mel elements. In NERWous C, there is a single mel element that resides somewhere on the NERW network, but there is no single global mel variable. Each task maintains its own global mel variable in its own individual context.


Global Variable Usage

We can see the following uses for global mel variables:
  1. Semantic Sharing
  2. Code Sharing
  3. Name Discovery
Semantic Sharing

In the example above, although both the GlobalStore and localStore mel elements are accessible by any task, their meanings are different. The GlobalStore mel element is meant to be a shared resource that is common to all tasks. On the other hand, the localStore mel element is controlled regionally by the main task. Only tasks that main wants them to work on localStore, will know about localStore that main explicitly passes as argument.

Code Sharing

Let's write a new Producer/Consumer example using only the global mel variable GlobalStore:
<mel> int GlobalStore;    /* global variable */
main () {
    <!> Producer ();
    <!> Consumer ();
    printReport ();
}
void Producer () {
    <extern> GlobalStore;
    while ( <?>GlobalStore = Produce() );
    printReport ();
}
void Consumer () {
    <extern> GlobalStore;
    while ( <?>GlobalStore )
        Consume ( GlobalStore<value> );
    printReport ();
}
void printReport () {
    printf ("[GlobalStore] status [%d], condition [%d]\n",
        GlobalStore<status>, GlobalStore<condition>);
}
The function printReport is a serial function. It is shared by all three tasks in the program (main, Producer and Consumer) to print out the status and condition of the global mel variable GlobalStore.

When invoked from the main task, printReport makes use of the global mel variable GlobalStore that is resident in the main context. This global mel variable is different from the global mel variables GlobalStore that are used by printReport invoked from the Producer and Consumer tasks. As said previously, each task maintains its own global mel variables in its own context. By using mel variables with the global scope, they do not need to explicitly pass them around to subsequent shared functions, giving the code a "cleaner" look.

Name Discovery

All mel variables, declared with either global or local scope, are reachable by any task in the NERWous C program, but only if the tasks know about them. For local mel variables, this knowledge comes from their explicit passing from parent tasks to children tasks as functional arguments. The global mel variables are also passed in a similar way, but implicitly via the <extern> statement. However, global mel variables have a feature that local mel variables does not, which is name discovery.

The CHAOS runtime environment keeps track of mel elements with global scope: their names and their contact information (such as the CORE properties). This allows tasks that do not get the mel element information passed to them from the parent task to ask CHAOS for the contact information so they can reach out to the mel element by themselves. This is done via the on attribute of the <snapshot> operation. We will see this use in the Extern Statement Translated section.

No Global Variables

Some programmers do not like the global-variable "cleaner" look. As a matter of style, they prefer not to deal with global variables at all, be it regular C variables or NERWous C mel variables. The above Producer/Consumer example can be rewritten by replacing global mel variables with local mel variables:
main () {
    <mel> int localStore;    /* local variable */
    <!> Producer (localStore);
    <!> Consumer (localStore);
    printReport (localStore);
}
void Producer (<mel> int store) {
    while ( <?>store = Produce() );
    printReport (store);
}
void Consumer (<mel> int store) {
    while ( <?>store )
        Consume ( store<value> );
    printReport (store);
}
void printReport (<mel> int store) {
    printf ("[%s] status [%d], condition [%d]\n",
        store<name>, store<status>, store<condition>);
}
Compared with the earlier global-variable version, the local-variable version looks a little more verbose with all the local mel variables being passed around. Interestingly, the local-variable version can be used to explain in detail the behavior of a global mel variable from creation to closure. As a matter of fact, a NERWous C compilation suite may support global mel variables by translating them into local mel variables that are passed around tasks behind the scene.


Global Variable Creation

When a local mel variable is created, it is under the context of the task that runs the mel variable declaration statement. What is the task context for a global mel variable? Let's modify our Producer/Consumer example again:
<mel> int GlobalStore;    /* global variable */
main () {
    if ( GlobalStore<error> ) {
        printf ("[GlobalStore] creation failed on error [%d] due to [%s]\n",
            GlobalStore<error>, GlobalStore<why>);
        exit (911);
    }
    <!> Producer ();
    <!> Consumer ();
}
void Producer () {
    <extern> GlobalStore;
    while ( <?>GlobalStore = Produce() );
}
void Consumer () {
    <extern> GlobalStore;
    while ( <?>GlobalStore )
        Consume ( GlobalStore<value> );
}
Like a local mel variable, a global mel variable declaration:
<mel> int GlobalStore;
is not only a compile-time statement but also a run-time operation to allocate shared resource to set up a mel element at a cel location. (In the above example, since the at attribute is not specified, the CHAOS runtime will allocate the mel element at a default cel that hosts generic shared resources.) This remote mel element is then represented for program use by the eponymous mel variable GlobalStore.

With the global mel variable GlobalStore defined outside of any function, its resident context is of a hidden startup task. This is the one that the CHAOS runtime uses to invoke the main task. It also passes to the main task any global mel variables that it creates. The properties of these global mel variables have the same initial property values for newly created local mel variables.

The hidden startup task is not meant to run a programmer's custom code. Therefore, if a programmer needs to check the result of a global mel creation, especially over an unstable runtime environment such as a wide area network (WAN) environment, this check has to be done in an explicit task. The main task is logically the closest one to the global mel creations, so it is it that usually does the checking, as in the example above. However any task that makes use of a global mel variable, can also does its own checking before use.

Multiple Global Variables

Multiple global mel variables can be declared for compile-time scope and run-time creation:
<mel> int GlobalStore1, GlobalStore2[5];
main () {
    if ( GlobalStore1<error> ) {
        printf ("[GlobalStore1] creation failed on error [%d] due to [%s]\n",
            GlobalStore1<error>, GlobalStore1<why>);
        exit (911);
    }
    if ( GlobalStore2<error> ){
         printf ("[GlobalStore2] creation failed on error [%d] due to [%s]\n",
            GlobalStore2<error>, GlobalStore2<why>);
         exit (911+1);
    }
}

No Exception Handling

The result of a local mel variable creation can be checked against the <error> property or caught via a mel exception. However due to the global mel creation being outside of any code block, the exception method is not available for global mel variables.

In the code snippet below, we can use the exception handling for the local mel creation but not for the global one:
<mel> int GlobalStore;
main () {
    if ( GlobalStore<error> ) {
        printf ("[GlobalStore] global creation failed on error [%d] due to [%s]\n",
            GlobalStore<error>, GlobalStore<why>);
        exit (911);
    }
    try { <mel> int localStore;
    } catch ( localStore<...> ) {
        printf ("[localStore] local creation failed on error [%d] due to [%s]\n",
            localStore<error>, localStore<why>);
        exit (911+2);
    }
}
For the global mel variable, we can only use the <error> property checking.

No Extern Statement

As said above, global mel variables are created under the context of the hidden startup task and then "passed naturally" to the main task. There is no need for an <extern> statement in the main task to receive all the declared global mel variables. This is not true when main needs to "pass" the global mel variables to its children tasks, as we will see next.


Global Variable Extern Statement

Except for the main task which receives the global mel variables from the hidden startup task, other tasks that need to use a global mel variable, have to explicitly invoke an <extern> statement on that global mel variable. Unlike the C language extern statement which is solely a compile-time statement to resolve scope, the NERWous C <extern> statement is also a run-time statement. It creates in the context of the task that runs that statement, a new mel variable that points to the same remote mel element as any other mel variable of the same name that appears in the context of any other task in that particular NERWous C program.

Let's modify our Producer/Consumer example to illustrate the <extern> use:
<mel> int GlobalStore;
main () {
    <!> Producer ();
    <!> DuoConsumers ();
    printReport ();
}
void Producer () {
    <extern> GlobalStore;
    while ( <?>GlobalStore = Produce() );
    printReport ();
}
void DuoConsumers () {
    <!> Consumer();
    <!> Consumer();
}
void Consumer () {
    <extern> GlobalStore;
    while ( <?>GlobalStore )
        Consume ( GlobalStore<value> );
    printReport ();
}
void printReport () {
    printf ("[GlobalStore] status [%d], condition [%d]\n",
        GlobalStore<status>, GlobalStore<condition>);
}
As said above, the <extern> statement in Producer creates a new GlobalStore mel variable in that task's context. How does that mel variable get initialized?

Extern Statement Signature

The Producer task indicates its intention to access the global mel variable GlobalStore via the statement
<extern> GlobalStore;
This statement updates the signature of the task Producer. As we will see in the pel basics chapter, a signature of a task consists of:
  1. Return values
  2. Explicit arguments
  3. Extern global variables
With its signature such updated, the Producer task requires any parent task that invokes it, to pass the global mel variable that the parent task maintains in its own context. Extern global variables can be viewed as implicit arguments, to complement the explicit functional arguments. (Neither the Producer nor Consumer versions here have functional arguments, see this example for the versions that have store as the functional argument.)

The <extern> statement is both declarative and operative. During compilation time, the <extern> declaration collects the type of the mel variable (such as int), and checks for the scope rules. During run time, the <extern> operation re-creates the global mel variable for the resident task. Like the local mel variables passed as functional arguments, the global mel variables created by the <extern> operation, will be initialized by value from the parent task.

In the latest example, there are five separate GlobalStore global mel variables. The first one is created in the context of the hidden startup task. It is then passed to the main task. This second version and the first version could be the same, depending on the NERWous C compilation suite. The third version is in the context of the Producer task. The fourth and fifth versions are in the context of each of the two Consumer tasks.

All the versions of the global mel variable GlobalStore have the same CORE properties since they access the same remote mel element. The other properties may diverge depending on what interactions the individual tasks have with the remote mel element.

All tasks share the same serial function printReport. It prints out the status and condition of the global mel variable in the context of the invoking task. It is invoked four times in the above example by different tasks, and the printf outputs can all be different.

Extern Operation Translated

NERWous C seeks to process global mel variables as much the same way as local mel variables. In fact, a NERWous C compilation suite may have a translation pass to transform global mel variables into local mel variables. A hypothetical translation could be as follows:
<mel> int GlobalStore;
main (int argc, char **argv, char **envp, char **nerw) {
    <mel> int __GlobalStore = nerw[0];
    <!> __Producer (__GlobalStore);
    <!> DuoConsumers ();
    __printReport (__GlobalStore);
}
void __Producer (<mel> int __GlobalStore) {
    if ( __GlobalStore == null ) <snapshot on="GlobalStore"> __GlobalStore;
    while ( <?>__GlobalStore = Produce() );
    __printReport (__GlobalStore);
}
void DuoConsumers () {
    <!> __Consumer(null);
    <!> __Consumer(null);
}
void __Consumer (<mel> int __GlobalStore) {
    if ( __GlobalStore == null ) <snapshot on="GlobalStore"> __GlobalStore;
    while ( <?>__GlobalStore )
        Consume ( __GlobalStore<value> );
    __printReport (__GlobalStore);
}
void __printReport (<mel> int __GlobalStore) {
    printf ("[GlobalStore] status [%d], condition [%d]\n",
        __GlobalStore<status>, __GlobalStore<condition>);
}
The hypothetical translation can help explain the working of the <extern> mel operation:
  1. The CHAOS runtime allocates shared resources for the mel element GlobalStore, and creates the eponymous mel variable with global scope.
     
  2. The CHAOS runtime passes this mel variable to the main task via the hidden nerw argument. This argument is similar to the customary argc, argv or envp that a C language runtime passes to the main function.
     
  3. The mel variable __GlobalStore that is passed to the main task contains properties with the creation initial values.
     
  4. With the appearance of the <extern> statement, the task function Producer is translated to be __Producer with <mel> int __GlobalStore as an explicit argument.
     
  5. When the main task invokes __Producer, it passes by value its __GlobalStore local variable to __Producer via the functional argument conduit.
     
  6. The task __Producer re-creates the __GlobalStore local mel variable in its own context, with properties set to passing initial values.
     
  7. When the task __Producer runs, it detects that there is a __GlobalStore mel argument passed to it, disowning the check for the null value and thus avoiding the call to the <snapshot> operation.
     
  8. The hypothetical NERWous C translator also detects the use of the global variable GlobalStore in the serial function printReport. It translates it to __printReport with the local mel variable __GlobalStore passed as argument. The serial function __printReport runs in the same task context as __Producer, and thus receives __GlobalStore by reference.
     
  9. The task function DuoConsumer does not have an <extern> statement. The hypothetical NERWous C translator does not rename it, and the main task invokes it without passing its local mel variable __GlobalStore over.
     
  10. On the other hand, the task function Consumer does include the <extern> statement, and has been translated it to be __Consumer with <mel> int __GlobalStore as its functional argument.
     
  11. Since DuoConsumer does not have a __GlobalStore local mel variable in its context, it invokes its two __Consumer tasks with the null value for the function argument.
     
  12. When each __Consumer task runs, it detects that there is no __GlobalStore passed to it. The check for the null value becomes affirmative, requiring the <snapshot> operation to be invoked with the on attribute.
     
  13. The value of the on attribute is GlobalStore, the official name of the globally scoped mel element. This instructs the CHAOS runtime to scan the NERWous C program's universe for a mel variable of the same name. If one is found, the local mel variable, __GlobalStore, is initialized with the snapshot values of that mel element.

An actual NERWous C compilation suite may or may not do the above translation. Nonetheless, the above translation describes the conceptual behavior of a global mel variable in NERWous C, as a roadmap for actual implementations.

Extern Statement Chain

The behavior of a global mel variable in NERWous C is like the behavior a local mel variable that is passed from task to task. This forms an extern statement chain. When the chain is broken, like the Duoconsumer above that skips the <extern> statement, the subsequent downstream task (here it is Consumer) has to run a discovery process to find out how to connect to the remote mel element represented by the global mel variable.

This discovery process is done under-the-hood by the CHAOS runtime via the Name Discovery process. This process could be quite expensive. If this were to impact the performance of a NERWous C program, it could be better to change the signature of the DuoConsumer task function so that the extern statement chain is maintained:
void DuoConsumers () {
    <extern> GlobalStore;
    <!> Consumer();
    <!> Consumer();
}

Extern Statement Short Passing

At run time, the <extern> statement re-creates the global mel variable in the context of the resident task. By default, this global mel variable will be created in full with the <value> and associated properties copied from the parent task (via explicit passing) or from the mel element itself by the CHAOS runtime (via the Name Discovery process). In many times, this copy is wasted since the resident task only needs some basic contact information to reach out to the remote mel element.

Like the short passing for local mel variables, a global mel variable can be initialized with short passing, by using the pass attribute to the <extern> statement:
<mel> int GlobalStore;
main () {
    <!> Producer ();
    <!> DuoConsumers ();
    printReport ();
}
void Producer () {
    <extern pass="short"> GlobalStore;
    while ( <?>GlobalStore = Produce() );
    printReport ();
}
void DuoConsumers () {
    <!> Consumer();
    <!> Consumer();
}
void Consumer () {
    <extern short> GlobalStore;
    while ( <?>GlobalStore )
        Consume (GlobalStore<value>);
    printReport ();
}
void printReport () {
    printf ("[GlobalStore] status [%d], condition [%d]\n",
        GlobalStore<status>, GlobalStore<condition>);
}
The term pass is optional, as seen in the <extern> statement for the task function Consumer.

For the Producer task, the main task is the parent task, and will pass the contents of its resident global mel variable GlobalStore over without the <value> and associated properties. The Producer task now has its own global mel variable GlobalStore re-created with the Short Passed Mel Argument Initial Property Values.

The Consumer task has its resident global mel variable GlobalStore re-created by the CHAOS runtime, also with the Short Passed Mel Argument Initial Property Values. This time, the initial values come fresh from the mel element itself, and not potentially stale from any parent task.

Extern Statement Multiple Values

Multiple global mel variables of the same data type and passing attribute can be grouped with the same <extern> statement.
<mel> int GlobalStore1, GlobalStore2, GlobalStore3;
main () {
    <!> Producer ();
    <!> DuoConsumers ();
    printReport ();
}
void Producer () {
    <extern> GlobalStore1, GlobalStore2;
    <extern pass=short> GlobalStore3;
    while ( <?>GlobalStore1 = Produce() &&
    <?>GlobalStore2 = Produce() );
    <?>GlobalStore3 = CloseEndProduce();
    printReport ();
}
void DuoConsumers () {
    <extern short> GlobalStore1, GlobalStore2, GlobalStore3;
    <!> Consumer(GlobalStore1);
    <!> Consumer(GlobalStore2);
    CloseEndConsume (<?>GlobalStore3);
    printReport ();
}
void CloseEndConsume (lastitem) {
 Consume (lastitem);
 <close> GlobalStore1;
 <close> GlobalStore2;
}
void Consumer (<mel> int store) {
    while ( <?>store )
        Consume ( store<value> );
}
void printReport () {
    printf ("[GlobalStore1] status [%d], condition [%d]\n",
        GlobalStore1<status>, GlobalStore1<condition>);
    printf ("[GlobalStore2] status [%d], condition [%d]\n",
        GlobalStore2<status>, GlobalStore2<condition>);
    printf ("[GlobalStore3] status [%d], condition [%d]\n",
        GlobalStore3<status>, GlobalStore3<condition>);
}
On each iteration, the single Producer task Produces two items and deposits them into the global mel elements GlobalStore1 and GlobalStore2. There are two Consumer tasks: one to Consume from GlobalStore1, and the other from GlobalStore2. When Producer ends its run, it generates a last product via CloseEndProduce. This product is not consumed by a Consume task, but by the DuoConsumers task.

After forking the two Consumer children tasks, the DuoConsumers task waits for the Producer task's CloseEndProduce run, via the GlobalStore3 global variable. It the invokes the CloseEndConsume serial function, that Consumes this last product, and invokes closure on the global variables that its Consumer child tasks are waiting for. This allows the Consumer tasks to break out of their infinite while loop via the CLOSURE exception.

Getting back to the Producer task, even though it only needs short passing for all three global mel variables, for illustration purpose, the above example has it make use of the full value passing for GlobalStore1 and GlobalStore2, and only short passing for GlobalStore3.

Extern Statement Macro

To keep the Extern Statement Chain unbroken, they need to be repeated in child tasks. To alleviate typing repetitions, C language macros can be used to group same-data-type global mel variables.
#define MYGLOBALS GlobalStore11,GlobalStore2,GlobalStore3
<mel> int MYGLOBALS;
main () {
    <!> Producer ();
    <!> DuoConsumers ();
    printReport ();
}
void Producer () {
    <extern> MYGLOBALS;
    while ( <?>GlobalStore1 = Produce() &&
    <?>GlobalStore2 = Produce() );
    <?>GlobalStore3 = CloseEndProduce();
    printReport ();
}
void DuoConsumers () {
    <extern short> MYGLOBALS;
    <!> Consumer(GlobalStore1);
    <!> Consumer(GlobalStore2);
    CloseEndConsume (<?>GlobalStore3);
    printReport ();
}
For illustration purpose, the Producer task requests full value passing from the parent task, while the DuoConsumer task only requests short passing. Both tasks make use of the macro MYGLOBALS to list the needed global mel variables.


Global Variable Closure

Any task that maintains its own resident global mel variable can invoke the <close> operation to close that global mel element. The first task that invokes the <close> operation removes all the resources that the CHAOS runtime has allocated to support shared access to the global-scoped mel element. Any subsequent access to the closed mel element by this task or any other task will result in a CLOSED mel operation exception.

We have seen the use of global mel closure in the CloseEndConsume function of an earlier example. Let's discuss closure with this simpler example:
<mel> int GlobalStore;
main () {
    <!> Producer ();
    <!> Consumer ();
}
void Producer () {
    <extern> GlobalStore;
    int c;
    while ( c = Produce() )
       <?>GlobalStore = c;

    <close>GlobalStore;
}
void Consumer () {
    <extern> GlobalStore;
    try {
        while ( 1 )
            Consume(<?>GlobalStore);
     } catch ( GlobalStore<CLOSED> ) {
         printf ("Consumer ends due to GlobalStore closed by Producer");
         return;
     }
}
The Producer and Consumer tasks communicate via the global mel element GlobalStore. Each maintains its own resident global mel variable that connects to the same remote mel element. When the Producer generates a 0-valued product, it stops Produce'ing more items. To inform the Consumer task that there are no more forthcoming products, it invokes the <close> operation on GlobalStore.

The Consumer keeps Consume'ing whatever coming up at the global mel element GlobalStore, until it gets hit by a <CLOSED> exception. With its understanding of the Producer behavior, the Consumer task knows that there will be no more products for it to Consume; it thus makes a printf notification, and then ends itself via the return statement.

The main task forks the Producer and Consumer tasks, then ends. The Producer task after closing GlobalStore also ends. The Consumer task after handling the <CLOSED> exception, also ends. With all the tasks having ended, the CHAOS runtime terminates the NERWous C program.


Previous Next Top