Welcome
» NERWous C
» Pel
Return Statement
The
The
Since the return signature of the function
Let's change the
Since the return value of a task is a readonly mel, multiple tasks can access it. For example, let's the
While both the
Since the
Although playing a subservient role to the pel feedback wait, the
Serial Return In Parallel
Sharp-eyed readers would notice that the exception handlers in the
Parallel Return In Serial
Once a function makes use of the
Pel'N'Back Shortcut
This statement seems to be buggy:
The pel'n'back shortcut is used when the code to be pelled has been written for task pelling, such as containing the
Return For Inline Code
Earlier, we see that
Let's put the
The
The local variables
Release Statement
In the previous examples, we use the
The
The
The
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
Multi-Value Return
In the above example, the
The multi-value feature is only supported for the NERWous C
Previous Next Top
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