Pages

Thursday, July 27, 2017

Pel Ending

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


Ways To End

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

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


End Statement

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

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

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

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

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

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

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

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

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


Terminate Statement

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

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

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

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

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

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

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


Pel TERMED Exception

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

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

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

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

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


Pel Ended Status

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

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


Pel ENDED Exception

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

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

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

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

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

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

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

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

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

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

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


Pel Ending Detection

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

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

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

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

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

Check the pel status

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

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

Inquire a pel exception

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


Kill Statement

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

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

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

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

Kill All Statement

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


Pel Abortion

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

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

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


Exit Statement

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

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

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

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

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

Exit Now Statement

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

Exit ASAP Statement

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

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

Multiple Exit Statements

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

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

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

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

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

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


Previous Next Top

No comments:

Post a Comment