Pages

Thursday, July 27, 2017

Pel Returns

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


Return Statement

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

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

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

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

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

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

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

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

Serial Return In Parallel

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

Parallel Return In Serial

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

Pel'N'Back Shortcut

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

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

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

Return For Inline Code

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

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

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

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

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


Release Statement

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

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

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

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


Multi-Value Release

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

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

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


Multi-Value Return

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

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


Previous Next Top

No comments:

Post a Comment