Welcome
» NERWous C
» Pel
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.
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
Let's see the
After pelling the Producer code block, the
The following statements are equivalent:
In the above example, the
Terminate Statement
In the Producer/Consumer example above, the Producer code block task keeps
In the example below, we introduce another way to end the Consumer task. It depends on the
If the
If the task targeted by the
To announce its presence, the
From the standpoint of the invoking task, issuing a
Pel TERMED Exception
In the example above, the
The
Once the
If
Pel Ended Status
Let's get back to the
Pel ENDED Exception
There are several ways for
From the pel side of NERWous C,
In the example below, let's combine all the ways:
The
By watching the
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
For example, if
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
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:
The statement
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
Check the pel status
The statement
To get the current status, the monitoring task can invoke the
Inquire a pel exception
The statement
Kill Statement
Let's go back to an earlier example where the
As discussed previously, the
Kill All Statement
The simple
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
Whenever the
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
The NERWous C
How does the CHAOS runtime ends all the tasks? It uses either the
Exit Now Statement
The
Exit ASAP Statement
The
The default behavior when no attribute is added, is the
Multiple Exit Statements
What happens if more than one task invokes the
If
Using the same logic as above, if
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
Previous Next Top
- Ways To End
- End Statement
- Terminate Statement
- Pel TERMED Exception
- Pel Ended Status
- Pel ENDED Exception
- Pel Ending Detection
- Kill Statement
- Pel Abortion
- 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. Thereturn
operation is the combination of the release operation and theend
operation. - A task is terminated if it has ended orderly due to a
terminate
command from another task. The task receiving theterminate
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.
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:
Mode | Synopsis |
---|---|
ENDED | This default mode is usually omitted. The task ends normally. Any tasks that are waiting on this task will get the ENDED exception. |
FAILED | This 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
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:
- The
kill
command does not allow the targeted task to clean up. Thekill
signal is sent to the CHAOS runtime directly. CHAOS immediately ends the targeted task.
- While
terminate
does not wait for the task to be terminated, thekill
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