Pages

Monday, March 6, 2017

Local and Global Variables

Welcome » NERWous C » Pel
  1. Local Variables
  2. Global Variables
  3. Import Attribute
  4. Import-File Attribute
  5. Array Variables
  6. Mel Variable Properties


Local Variables

In the previous chapter, we say that in order to run something in parallel, we just pel it with the <!> construct. Actually, things are not that simple. For example, can we use local variables from the parent task? Let's take a look at this serial program:
/* VERSION 1 */
#define MAX 100
main () {
    int store[MAX];
    int counter;

    /* Producer first */
    counter = MAX;
    while ( --counter )
     store[counter] = Produce();

    /* Consumer next */
    counter = MAX;
    while ( --counter ) 
        Consume(store[counter]);
}
This contrived example uses a local variable, counter. It is used first in the Producer code to load the array store with Produced items. It is then used again in the Consumer code to unload the store array to Consume.

Let's change the Producer and Consumer codes to run in parallel, and see how the local variable counter behaves.
/* VERSION 2 */
#define MAX 100
main () {
    int counter = MAX;
    <mel buffer=counter/10> int store;

    <!> {    /* Producer task */
        while ( --counter ) 
            <?>store = Produce();
    }

    <!> {    /* Consumer task */
        while ( --counter ) 
            Consume(<?>store);
    }
}
First we transform the store array into a mel variable so that it can be shared between the Producer task and Consumer task. To increase concurrency, we give it a buffer with an arbitrary size of 10% of the number of iterations maintained by counter. Then we consecutively pel the code blocks for Producer and Consumer. The main task then ends. The whole program will exit at a later time when the Producer and Consumer tasks have also ended.

The item of interest here is the local variable counter. It appears in both pelled tasks. Unlike the serial VERSION 1 example, the value of counter when it gets to the Consumer task is still MAX as originally set by main. The reduction of counter in the Producer task (via the statement --counter) is done separately in that task and does not impact the counter in the main task. In fact, there are three distinct local counter variables in the VERSION 2 example, one for main, one for Producer, and one for Consumer.

During compile time, the NERW translator will analyze any block of code that is under the pel <!> construct. Any local variables from the pelling code (which is main here) that are re-used in the pelled code, will be duplicated in the pelled tasks. The values of the duplicated local variables will be the same as of the ones of the pelling code at the time of the pelling. This is why Consumer has its own counter variable and it is valued with the MAX value, same as Producer.

Let's look at another version:
/* VERSION 3 */
#define MAX 100
main () {
    int counter = MAX;
    int numtasks = 0;
    <mel buffer=counter/10> int store;

    <!> {    /* Producer task */
        while ( --counter )
            <?>store = Produce();
    }
    ++numtasks;

    <!> {    /* Consumer task */
        printf ("This is task [%]", numtasks);
        while ( --counter )
            Consume(<?>store);
    }
    ++numtasks;

    printf ("Waiting for %d pelled tasks to terminate", numtasks);
}
Like counter, numtasks is also a local variable of main. Unlike counter though, numtasks is not used in the Producer pelled code block. Thus, the NERWous C translator during compilation time will not generate behind-the-scene code to duplicate this variable for the Producer task. On the other hand, the Consumer pelled code block does use numtask. The NERWous C translator can detect this use during compilation, and will generate code to create a separate numtask local variable for the Consumer task. It also initializes this local variable with the value of numtask from the main task at the time of the pelling (which is 1 in the example above).

To support local variables, the compile-time NERWous C translator needs to be able to:
  1. re-create any local variables from the pelling task to the pelled task if they also appear in the pelled code as right-hand-side values,
  2. initialize these created local variables with the values of the eponymous local variables from the pelling task at the time of pelling.
An implementation of the NERWous C translator may initialize all duplicated local variables intead of just the ones used in the right-hand side. Such implementation may make sense in a single-server multi-threaded environment where local variables in a stack can be copied in bulk from one threaded task to another threaded task. For tasks that are distributed in separate nodes, especially over a wide area network, the cost of transferring data to initialize un-needed left-hand-side local variables may impact performance. Thus, the requirement for the NERWous C translator is to initialize only right-hand-side local variables.

Let's rewrite the above example, replacing code blocks by functions:
/* VERSION 4 */
#define MAX 100
main () {
    int counter = MAX;
    int numtasks = 0;
    <mel buffer=counter/10> int store;

    /* Producer task */
    <!>Producer (store, counter);
    ++numtasks;

    /* Consumer task */
    <!>Consumer (store, counter, numtasks);
    ++numtasks;

    printf ("Waiting for %d pelled tasks to terminate", numtasks);
}

void Producer (mel int store, int counter) {
    while ( --counter ) 
        <?>store = Produce();
}

void Consumer (mel int store, int counter, numtasks) {
    printf ("This is task [%]", numtasks);
    while ( --counter ) 
        Consume(<?>store);
}

With the Producer and Consumer tasks encapsulated in functions, whatever local variables from the main task are needed, have to be explicitly passed as functional arguments. Again, the values of the arguments are the values of the local variables of the pelling task at the time of pelling. In the example above, counter has the value of MAX for both Producer and Consumer tasks, and numtasks has the value of 1 on entry of the Consumer task.


Global Variables

In the previous examples, we look at local variables. In the following examples, let's consider global variables with pel statements. We start with a serial C program that contains three global variables:
/* VERSION 5 */
#define MAX 100
int Counter;
int Item;
int Version = 5;
main () {
    int store[MAX];

    /* Producer first */
    Counter = MAX;
    while ( --Counter ) {
        Item = Produce ();
        store[Counter] = Item;
        REPORT ("Producing");
    }

    /* Consumer next */
    Counter = MAX;
    while ( --Counter ) {
        Item = store[Counter];
        Consume(Item);
        REPORT ("Consuming");
    }
}

function REPORT (action) {
    printf ("Version [%d]: Action [%s], Counter [%d] Item [%d]\n", Version, action, Counter, Item);
}
The three global variables, Version, Counter and Item, are used so that we don't need to pass them to the REPORT function as arguments. (This may not be a good way to write a computer program, but we need something simplistic to discuss the complex issue of global variables.)

Now let's rewrite the Producer and Consumer code blocks to run in parallel:
/* VERSION 6 - BUGGY */
#define MAX 100
int Version = 6;
int Counter = MAX;
int Item;
main () {
    <mel buffer=counter/10> int store;

    /* Producer task */
    <!> while ( --Counter ) {
       Item = Produce ();
       <?>store = Item;
       REPORT ("Producing");
    }

    /* Consumer task */
    <!> while ( --Counter ) {
       Item = <?>store;
       Consume(Item);
       REPORT ("Consuming");
    }
}

function REPORT (action) {
    printf ("Version [%d]: Action [%s], Counter [%d] Item [%d]\n", Version, action, Counter, Item);
}
Like local variables, the global variables are automatically made available in the pelled tasks. The above example contains 3 sets of Version, Counter and Item The first set is in the main task, the second in the Producer task, and the third in the Consumer task. Each set is independent to each other, and is used for localized global access within a task only.

In NERWous C, a global variable is different from a memory element (mel) variable. A global variable is globally accessible in the compile-time scope, while a mel variable is globally accessible in the run-time scope. A compile-time scope means that a global variable can be accessed anywhere in the program: it is declared once and any block of code or function can refer to it with the help of the language compiler. A run-time scope means that a mel variable can be shared by different tasks that are forked (or in NERWous C parlance, pelled) during the running of the program. The shared access is provided by the CHAOS runtime environment. In the example above, the mel variable store is shared between the Producer task and the Consumer task. Note that store is declared as a local variable and not as a global variable for the compile-time scope.

This is how the NERWous C translator handles global variables at compile time:
  1. The translator collects the types and names of all global variables in the pelling task.
     
  2. When the translator first sees a variable in a pelled task and this variable is not a local variable, the translator automatically creates a global variable in the pelled task environment, using the type of the eponymous global variable collected from the pelling task.
     
  3. While the translator automatically re-creates global variables for pelled tasks, the translator does not automatically initialize them, unless the global variables are included in the import or import-file attribute.
The reason the translator does not do automatic initialization of re-created global variables for pelled tasks, while local variables are both re-created and initialized, is due to performance.

It can be hard to detect what global variables a pelled task will use, since global variables can appear in functions called deep in the execution thread starting from the first level of the pelled task. This differs from local variables where they only appear in the first level of a pelled code block -- which is easier to detect. Automatically initializing all global variables during run-time for any created pel task even if it never makes use of all of them, will likely impact the performance of a concurrent NERWous C program, especially when the tasks are distributed over a wide area network, and some global variables contain huge data.

The NERWous C translator will automatically create global variables in pelled tasks as they are needed, but initializes them only on demand, via the import or import-file attribute. In the example above, neither of these attributes is used, resulting in the program being "buggy". For example, the global variable Counter is re-created in the Producer and Consumer tasks, but neither of these copies are initialized. Thus the statement:
while ( --Counter ) {
will fail because Counter is undefined.

The import feature allows the pelling task to send data to the pelled task to initialize global variables. There is no export feature for the pelled task to update back the global variables in the pelling task. For this, the global variables must be declared as mel variables.


Import Attribute

Let's fix the above "buggy" example by making use of the import attribute to the pel <!> statement:
/* VERSION 7 */
#define MAX 100
int Version = 7;
int Counter = MAX;
int Item;
main () {
    <mel buffer=counter/10> int store;

    /* Producer task */
    <! import="Version,Counter"> while ( --Counter ) {
       Item = Produce ();
       <?>store = Item;
       REPORT ("Producing");
    }

    /* Consumer task */
    <! import="Version,Counter"> while ( --Counter ) {
       Item = <?>store;
       Consume(Item);
       REPORT ("Consuming");
    }
}

function REPORT (action) {
    printf ("Version [%d]: Action [%s], Counter [%d] Item [%d]\n", Version, action, Counter, Item);
}
The import attribute to the pel construct <!> instructs the NERWous C translator to generate behind-the-scene code to re-create the specified global variables (i.e. Version and Counter) in the pelled task, and to also initialize them with the current values of the same-named variables from the pelling task.

The global variable Item is not in the import list and is not created at the time of the pel statement. Only when the NERWous C translator encounters Item the first time in the Producer code block (or Consumer code block), then it will create a global variable for Item localized for the pelled task. That means that the Item created for the Producer task is different from the one created for the Consumer task, and both of them different from the one declared in the main task. The only sameness between these different global variables is the type (which is int) that the NERWous C translator knows from parsing the whole program.

Global variables, like Item, that are created on a needed basis, will not be initialized. The current values in the pelling task are not transferred to the pelled task. This is beneficial, not detrimental. In the example above, there is no need to transfer the value of Item over from the pelling task, and have the pelled task overrides it with the result of Produce() or the value of the mel store. Via the import attribute, the programmer can specify only the global variables whose values need to be transferred over.

What happens if the programmer forgets a global variable in the import list, such as in this code fragment:
<! import="Counter">
Since Version is not imported, it is created uninitialized on a needed basis. On the above example, this will be when the function REPORT is invoked. The printf will display the indeterminate value of the uninitialized Version, instead of the static value 7 from the main task.

The import and import-file (to be introduced next) attributes support one-way transfer of data from the pelling task to the pelled task, Any changes that the pelled task does to its copy of the global variables stay with the pelled task and not copied back to the pelling task. If this update is desired, then the global variables should be declared as mel global variables whom read/write access is shared to multiple tasks, including the pelling task.


Import-File Attribute

The import attribute is fine when we have a few global variables to transfer over. When the list grows long, it becomes hard to maintain. In this case, it may be better to put this list into a separate file and use the import-file attribute to the <!> pel statement.

In this import file, procon.h, we introduce the global variables to be imported with the C language extern:
/* procon.h */
extern int Version;
extern int Counter;
Let's modify our example to use procon.h:
/* VERSION 8 */
#define MAX 100
int Version = 8;
int Counter = MAX;
int Item;
main () {
    <mel buffer=counter/10> int store;

    /* Producer task */
    <! import-file="procon.h"> while ( --Counter ) {
       Item = Produce ();
       <?>store = Item;
       REPORT ("Producing");
    }

    /* Consumer task */
    <! import-file="procon.h"> while ( --Counter ) {
       Item = <?>store;
       Consume(Item);
       REPORT ("Consuming");
    }
}

function REPORT (action) {
    printf ("Version [%d]: Action [%s], Counter [%d] Item [%d]\n", Version, action, Counter, Item);
}
To find the location of procon.h the NERWous C translator follows the same resolution rule used by the C preprocessor for handling:
#include "procon.h"

The same file inclusion works similarly when the pelled tasks are encapsulated as functions:
/* VERSION 9 */
#define MAX 100
int Version = 9;
int Counter = MAX;
int Item;
main () {
    <mel buffer=counter/10> int store;

    /* Producer task */
    <! import-file="procon.h"> 
        Producer (store);

    /* Consumer task */
    <! import-file="procon.h"> 
        Consumer (store);
}

void Producer (mel int store) {
    while ( --Counter ) {
        Item = Produce ();
        <?>store = Item;
        REPORT ("Producing");
    }
}

void Consumer (mel int store) {
    while ( --Counter ) {
       Item = <?>store;
       Consume(Item);
       REPORT ("Consuming");
    }
}

function REPORT (action) {
    printf ("Version [%d]: Action [%s], Counter [%d] Item [%d]\n", Version, action, Counter, Item);
}
For both the Producer and Consumer tasks, their localized version of the global variables Counter and Version are created and initialized with the values from the main task right at the pel <!> statement. However they are not put to use until the execution flows into the Producer or Consumer function.

We can import multiple files with the import-file attribute:
<! import-file="file1.h, file2.h">

We can mix the import-file attribute with the import attribute:
<! import-file="file1.h, file2.h" import="var1, var2">
The imported files usually contain groups of global variables that are commonly imported by many tasks, while the individual imported global variables are specific to a certain task.


Array Variables

The previous examples use simple data types for the global variables. Now let's explore the use of arrays as variables, either local or global. If the whole array is to be imported to the pelled task, then an array variable is behaving no differently than a simple-typed variable. What interesting is the import facility allowing the transfer of a subset of the array to a pelled task.

Let's modify the above example again:
/* VERSION 10 */
#define MAX 100
#define BUNCH 5
int Version = 10;
int Counter;
int Item;
main () {
    int store[MAX];

    /* Producer task - run in serial */
    for (Counter=0; Counter<MAX; ++Counter) {
       Item = Produce ();
       store[Counter] = Item;
       REPORT ("Producing");
    }  

    /* Consumer task - run in parallel */
    for (int i=0; i<MAX; i += BUNCH)
    <! import="Version, store[i] ... store[i+BUNCH-1]"> {
       for (Counter=i; Counter<i+BUNCH; ++Counter) {
          Item = store[Counter];
          Consume (Item);
          REPORT ("Consuming");
       }
    }
}

function REPORT (action) {
    printf ("Version [%d]: Action [%s], Counter [%d] Item [%d]\n", Version, action, Counter, Item);
}
First, the mel array store is converted to a local array variable. The Producer then seeds this array with a seqential for loop. Once the array is seeded, it will be consumed in parallel, in a BUNCH items at a time. The main task uses the LIST (...) construct:
store[i] ... store[i+BUNCH-1]
to pass BUNCH items for the pelling task to use.

Note the use of the local variable i in these three consecutive lines:
    for (int i=0; i<MAX; i += BUNCH)
    <! import="Version, store[i] ... store[i+BUNCH-1]"> {
       for (Counter=i; Counter<i+BUNCH; ++Counter) {
The i variables appearing in the outer for statement and the import attribute are the same, and reside in the main task. On the other hand, the i used in the for loop belongs to the corresponding pelled task, and each pelled task has its own local version of i. Although they reside in different tasks than the main task, they get initialized with the value of the i variable of the main task at the time of the task creation.

We "cheat" a little bit in the previous example by making MAX divisible to BUNCH so that we don't have to worry the last iteration not having all BUNCH number of store elements. Let's update the pelling code to handle this boundary condition:
/* Consumer task - run in parallel */
    for (int i=0; i<MAX; i += BUNCH) {
        int upperbound = i+BUNCH-1;
        if ( upperbound >= MAX ) upperbound = MAX-1;
        
        <! import="Version, store[i ... upperbound]"> {
          for (Counter=i; Counter<=upperbound; ++Counter) {
              Item = store[Counter];
              Consume (Item);
              REPORT ("Consuming");
          }
        }
    }
Like the i variable, upperbound is also a local variable. This allows it to be automatically duplicated to the pelled task and be used there without much ado.


Mel Variable Properties

The properties of a mel variable are information about that mel variable that are cached locally in the requesting task so that they don't have to be constantly retrieved from the remote mel location. The mel properties of a mel variable have the same compile-time local or global scope as of that mel variable:
<mel> int store1;
main () {
    <mel> int store2;
    printf ("In [main], store1 buffer is [%d]", store1<buffersize>);
    printf ("In [main], store2 buffer is [%d]", store2<buffersize>);
 
    <!> {
        printf ("In pelled task, store1 buffer is [%d]", store1<buffersize>);
        printf ("In pelled task, store2 buffer is [%d]", store2<buffersize>);
    }
}
The store2 variable is declared as a local variable, and so are its properties. The properties of store2 are therefore automatically transferred from the main task to the pelled task. The printf statemetns for store2 will show the same buffer size.

Interestingly, both printf statements for store1 also show the same buffer size, even though store1 is a global variable. Its properties are thus also global, and we have been said that global variables are not initialized automatically in the pelled task. What was said is correct for regular variables, but not true for mel properties.

When the store1 is created in the statement:
<mel> int store1;
its properties are initialized with the current information of the newly created mel entity. The NERWous C translator, during compile time, sees that the main task makes use of the store1 variable, and so it adds an implicit import operation of store1 into main. This import is executed at run-time when main is created.

When main pels the inline task, the properties cache that main currently maintains for both store1 and store2 will be passed to the inline task. Let's expand the above example:
<mel> int store1;
main () {
    <mel> int store2;
    printf ("In [main], store1 buffer is [%d]", store1<buffersize>);
    printf ("In [main], store2 buffer is [%d]", store2<buffersize>);
    <rebuffer buffer+10> store1;
    <rebuffer buffer+10> store2;

    <!> {
        printf ("In pelled task, store1 buffer is [%d]", store1<buffersize>);
        printf ("In pelled task, store2 buffer is [%d]", store2<buffersize>);
    }
}
The printf statements now show different values. While those from main show the original buffer sizes of the mel variables, the ones from the pelled task show the buffer sizes as having been incremented by 10 due to the rebuffer operations. These values are what main have in the properties caches when it pels the inline task.

The reason for mel properties to be treated the same for both local and global scopes with automatic initialization is that the information on how to access the remote mel must be passed to the tasks so that they can do mel operations such as reads and writes. The rest of the properties thus piggy-backs on that. If programmers have to explicitly specify the import attribute for global mel variables every time they make use of the pel <!> statement, a NERWous C program would become unwieldy.

How about performance? Unlike regular variables which contain data, mel properties only contain metadata (i.e. the properties). The real data of a mel is stored centrally in the cel that hosts that mel. The local properties cache of each mel variable is quite manageable, and can be transferred to a pelled task without undue impact on performance.


Previous Next Top

Tuesday, January 24, 2017

Pel Basic

Welcome » NERWous C » Pel
  1. Pel Construct
  2. Pel Code Blocks
  3. Pel Life Cycle
  4. Pel Variables
  5. Synchronous Task Creation
  6. Pel Operations
  7. Pel Properties
  8. Pel Exceptions


Pel Construct

Let's repeat the Producer / Consumer example we first encounter with the introduction to the NERWous C mel construct. First, let's review the serial program written in standard C language, with a while loop of Produce and Consume until no more products (with Produce returning 0):
main () {
     int c;
     while (c = Produce())
           Consume(c);
}
This is the NERWous C version as we have seen before:
/* VERSION 1 */
main () {
    <mel> int store;
    <!> Producer (store);
    <!> Consumer (store);
}
void Producer (<mel> int store) {
    while ( <?>store = Produce() );
}
void Consumer (<mel> int store) {
    while ( int c = <?>store )
        Consume(c);
}
The main task creates two parallel tasks, Producer and Consumer, and the mel variable store as the communication channel between the two. The task creation is done via the pel construct, <!>.

The pel construct is both a compile-time and run-time artifact. During compilation, the NERWous C translator will replace the pel construct with code native to the targeted platform. When the compiled program is run, the CHAOS runtime environment will execute this code to have the task created as a thread, job, remote process, web service or whatever execution unit the targeted platform supports. The created task then runs the code indicated by the pel statement. In the example above, it is the functions Producer and Consumer.

Under NERWous C, a task creation is asynchronous. A pel statement is a request sent to CHAOS to have a task created. As long as CHAOS acknowledges this request, the pel statement can return to the caller. Depending on the platform and implementation, CHAOS may create the task right away or schedules the creation at a later time. The benefit of an asynchronous task creation is that it optimizes the overall performance of the program, especially if the execution unit of the targeted platform is "heavy-weighted", requiring substantial resources to set up.

Synchronous task creations are also available by using pel variables.


Pel Code Blocks

In VERSION 1 above, the pelled code is encapsulated in a function. This does not have to be so. Any code block can be tagged with the pel construct <!>, in order to be assigned to a task and run in parallel:
/* VERSION 2 */
main () {
    <mel> int store;

    <!> {    // Inline Producer
        while ( <?>store = Produce() );
    }

    <!> {    // Inline Consumer
        while ( int c = <?>store )
            Consume(c);
    }
}
VERSION 2 inline programming style is suitable mostly for small blocks of code. For complicated programs, the distinct separation offered by a function provides a friendlier way to identify the various tasks.

The creation of the pelled code block is also asynchronous. Once the main task has registered its pel request for the producer code block with the CHAOS runtime, it continues by registering the pel request for the consumer code block. It does not wait for either pelled code blocks to finish.

Besides stand-alone code blocks, the pel construct can be applied to for and while loops.


Pel Life Cycle

A task is open for execution when the CHAOS runtime creates it upon the pel command <!> request. It runs in the assigned computer element (cel) until it is ended. When all the tasks in the program have ended, the NERWous C program exits.

There are several ways for a task to end:
  1. A task runs out of things to do by falling off the last code statement.
     
  2. A task invokes the return or <return> statement, to release an optional value to the calling task before ending itself.
     
  3. A task invokes the <end> statement to end itself with an exception.
     
  4. A task can be terminated by another task. The terminated task can capture this exception, and ends itself orderly.
     
  5. A task can be killed by another task. The killed task ends abruptly without the chance for an orderly ending.
     
  6. A task can be aborted due to an unhandled exception or code crash.


Pel Variables

A pel variable allows the pelling task to monitor and act on its pelled task. Let's modify VERSION 1 of the Producer/Consumer example to make use of a pel variable:
/* VERSION 3 */
main () {
    <mel> int store;
    <pel>prod = <!>Producer (store);
    <!>Consumer (store);
}
void Producer (<mel> int store) {
    while ( <?>store = Produce() );
}
void Consumer (<mel> int store) {
    while ( int c = <?>store )
        Consume(c);
}
A pel variable is introduced with the <pel> construct. It is a local variable residing with the creating parent task. It contains localized information about the child task during creation, execution and ending.

There is a significant difference between VERSION 1 and VERSION 3. In VERSION 1, both tasks are created asynchronously. The main task pels the Producer task and immediately issues the pel request for the Consumer task -- it does not wait to see if the Producer task has been properly created or not. This results in two eventualities:
  1. The Producer task may fail to be created, due to lack of resources or other reasons, without the main task knowing about it.
  2. The Consumer task may be created and runs before the Producer task. This could happen if the CHAOS runtime were to assign Consumer to a faster processing unit, or implement a Last-In-First-Out scheduling scheme.
These two eventualities can be resolved by assigning a pel variable to the pel statement to force a synchronous task creation, as seen in VERSION 3.


Synchronous Task Creation

Assigning a pel variable to a pel statement transforms the asynchronous task creation into a synchronous one. This is because the pel variable must be initialized with the result of the task creation. In order to do that, the CHAOS runtime environment has to finish creating the task first to collect the required information.

In VERSION 3, the main task issues a pel request to create Producer and waits for it to be created via the pel variable prod. To know the result of this operation, it checks the status of the pel variable:
/* VERSION 4 */
main () {
    <mel> int store;
    <pel>prod = <!>Producer (store);
    if ( prod<status> == NERW_STATUS_FAILED ) {
        printf ("Producer failed to be created due to %s", prod<why>);
        exit (1);   // exit without invoking Consumer
    }
    <!>Consumer (store);
}
The pel variable prod provides an attachment point for the pel operation to report the result of its attempt to create a task to run the pelled code. In a distributed wide-area-network environment (such as the Internet), the main task has to wait for the remote cel site to report if the task has been successfully created there or not.

Since the creation of the Producer task is synchronous, it is guaranteed to be created before the Consumer task. In this chapter's Producer/Consumer examples, it does not matter if Consumer were to run before Producer because it will wait for the mel store to be valued with a product anyway. However in other cases, the sequence of task creations may be significant, such as in this mel example. In this and similar cases, the use of pel variable assignments is needed.


Pel Operations

A pel operation is a request that a parent task sends to the CHAOS runtime to affect its child task. In NERWous C, a pel operation is specified within the < > markers, and placed before a pel variable representing the created child task. The exception is the pel creation operation where the optional pel variable is the left-hand-side value of an assignment. An operation can contain attributes to fine tune the request.

These are the pel operations and their attributes, using the pel variable p as an example:
OperationAttributesSynopsis
<!>code at
=cel
Request that code runs on the specific cel
import
=list-of-vars
Transfer the values of specific global variables to the created task
import-file
=list-of-files
Transfer the values of specific global variables in the listed files to the created task
mode
=setting
Request that the task be created with this mode: suspend (create then suspend execution), quickstart (create faster than normal), maintenance (create in maintenance mode for diagnostic messages), running (create and wait for the task to run)
name
=usn
Assign a unique user-specified name so the task can be referred to without knowing the system-generated task ID.
priority
=n
Request that code runs at the n-th priority level
timeout
=t
(Synchronous mode only) Request that the task be created within t msec.
<start>p Start a suspended task.
<suspend>p Suspend a running task.
<update>p name=usn Update the local properties of task p. If the name attribute is specified, update p from the task named with the specified user-specified name.
<terminate>p Request that the task p be terminated.
<kill>p Force the task p to end immediately.



Pel Properties

A property of a pel variable is a localized value that the parent task caches from a current or past interaction with its child task. In NERWous C, a property is specified in lower case within the < > markers, and placed after a pel variable.

These are the pel properties of the pel variable p:
PropertySynopsis
p<location>Cached value of the cel the task runs on
p<priority>Cached value of the task priority
p<status>Status of the task from the last update operation
p<why>Reason for the task fails

A pel property value is only valid from the last interaction between the parent task and the child task. For example, the child task may have terminated but its status property at the parent task may still say "running", until the parent task does an update operation or interacts in other ways with the child task.


Pel Exceptions

A pel operation can fail or the child task can fail. In NERWous C, such failure generates a pel exception. A pel exception is specified within the < > markers, and placed after a pel variable, like a pel property. Unlike pel properties which are specified in lower case, pel exceptions names are upper cased.

A programmer can encapsulate a pel operation with a try/catch construct to capture the pel exceptions. These are the pel exceptions:
ExceptionSynopsis
p<FAILED>The task has ended abnormally
p<ENDED>The task has ended normally
p<TIMEOUT>The task creation request has timed out

If a pel exception has fired, the pel property <status> will contain the name of the exception:
/* VERSION 5 */
main () {
    <mel> int store;
    try {
        <pel>prod = <!>Producer (store);
    }
    catch (prod<FAILED>) {
        printf ("Producer failed to be created due to %s", prod<why>);
        printf ("The job status [%s] should be FAILED", prod<status>);
        exit (1);   // exit without invoking Consumer
    }
    <!>Consumer (store);
}



Previous Next Top

Sunday, January 22, 2017

GPU Accelerator Offloading

Welcome » NERWous C » Examples
  1. Current Literature
  2. NERWous C Sample


Current Literature

Recent computer design has a general-purpose central processing unit (CPU) and a dedicated graphics processing unit (GPU). Due to its need to render graphics, a GPU crunches numbers at speed of several magnitude over a CPU. A program can therefore offloads the heavy mathematical computation part to the GPU and retains the logical processing part at the CPU.

As an example of this workload division, we will do a matrix addition:
Two matrices must have an equal number of rows and columns to be added. The sum of two matrices A and B will be a matrix which has the same number of rows and columns as do A and B. The sum of A and B, denoted A + B, is computed by adding corresponding elements of A and B.

An example of offloading matrix addition to a GPU is described in C++ - A Code-Based Introduction to C++ AMP. C++ Accelerated Massive Parallelism (C++ AMP) is Microsoft solution to write a program such that portions of it can be compiled and executed on data-parallel hardware. This is the C++ AMP code for matrix addition:
#include                 // C++ AMP header file
using namespace concurrency;    // Save some typing :)
using std::vector;     // Ditto. Comes from  brought in by amp.h

int main()
{
  // Rows and columns for matrix
  const int M = 1024;
  const int N = 1024;

  // Create storage for a matrix of above size
  vector vA(M * N);
  vector vB(M * N);

  // Populate matrix objects
  int i = 0;
  std::generate(vA.begin(), vA.end(), [&i](){return i++;});
  std::generate(vB.begin(), vB.end(), [&i](){return i--;});

  // Output storage for matrix calculation
  vector vC(M * N);

  perform_calculation(vA, vB, vC, M, N);
}
void perform_calculation(
     vector& vA, vector& vB, vector& vC, int M, int N)
{
   extent<2> e(M, N);
   array_view a(e, vA), b(e, vB);
   array_view c(e, vC);

   index<2> idx(0, 0);
   parallel_for_each(e, [=](index<2> idx) restrict(amp)
   {
       c[idx] = a[idx] + b[idx];
   }
}
The description of the C++ AMP specific constructs -- extent, array_view, index and parallel_for_each -- can be read from the reference. Here we are interested in the restrict construct. C++ AMP supports restrict(cpu) for code to be run on the main CPU. This is the default. For code to be offloaded to a hardware accelerator, it is annotated with restrict(amp). During compilation time, the annotated code will be checked against the instruction set that such accelerator can support.


NERWous C Sample

Let's write the matrix addition in NERWous C. The additions will be run in parallel on a cel location called GPU. Since the input matrixes, vA and vB, are to be accessed by multiple tasks, they are declared as mel shared variables. Since, for matrix addition, each element of the input matrixes are added together, vA and vB are declared as mel arrays to facilitate individual access.
extern <cell> GPU
#define M 1024
#define N 1024
int main() 
{
  // Create matrix objects in shared storage
  <mel> int vA[M][N];    // input
  <mel> int vB[M][N];    // input
  <mel> int vC[M][N];    // output

  // Populate matrix objects serially
  int acount = 0;
  int bcount = M*N;
  for (int i = 0; i < M; i++)
  {
     for (int j = 0 ; i < N; j++) 
     {
         <?>vA[i][j] = acount++;
         <?>vB[i][j] = bcount--;
     }
   }
  
  perform_calculation(vA, vB, vC, M, N);
}

void perform_calculation(
     <mel> int vA[][], <mel> int vB[][], <mel> int vC[][], int M, int N)
{
    // Compute additions in parallel on the GPU
    for (int i = 0; i < M; i++) 
    <collect> {
       for (int j = 0 ; i < N; j++) 
       <! at=GPU> {
          <?>vC[i][j] = <?>vA[i][j] + <?>vB[i][j];
       }
    } <? ENDED>;  /* make sure that all tasks have ended */
}
The population of the matrix object is done serially to guarantee that the local variables acount and bcount are respectively incremented and decremented sequentially.

In the perform_calculation function, we use two for loops to iterate through all the elements of the matrix objects. The first for loop runs on the main CPU using whatever supported thread facility. The purpose of this loop is only to invoke the inner loops. The inner for loops have all the tasks running on the GPU cel since they are doing "computational heavy" arithmetic operation. (The addition operation here is of course not "computational heavy", but you get the idea.)

The perform_calculation function embeds the for loops inside a collect-ENDED wrapper so that it can wait for all the tasks it pels to have ended before it does a function return.

How is the cel GPU defined? It is defined as extern in the beginning of the NERWous program. This means that its definition comes from the configuration file associated with the NERWous program. In a truly "Accelerated Massive Parallelism" environment, this definition will point to the installed GPU, and the NERWous program will be compiled with a corresponding AMP library and a pre-processor that verifies that all the NERWous C code assigned to the cel GPU can be executed on the real GPU.

The programmer can associate a different configuration to the NERWous C program, such as one where the cel GPU is a web site with SOAP access. A different library and pre-processor will be used in this case, but the NERWous C program will stay the same.


Previous Next Top

Monday, December 5, 2016

Mel Writer Zones

Welcome » NERWous C » Mel
  1. Writer Zones
  2. Checkout Operation
  3. Writer OR Zones
  4. Writer LIST Zones
  5. Writer AND Zones
  6. Writer Zone Attributes


Writer Zones

While the reader zone from previous chapters deal with the reader's end of a mel buffer, we now introduce the mel writer zones for exclusive access to the writer's end of the mel buffer. We are using the term "mel buffer" even if the mel variable is not buffered (which is the default of a mel declaration) -- an unbuffered mel is a mel with a buffer of one slot.

Let's create a new task called Generator that wants exclusive access to the mel buffer to Generate and deposit new products. We still have our customary Producer task that has been generating "mainstream" products. This Generator task is used to insert some "rogue" products into the production line.
/* Generator - VERSION 1 */
void Generator(<mel> int store) {
    try {
      <?writer>store {
          store = Generate();
      }
    }
    catch ( store<CLOSED> ) {}
}
A writer zone is differentiated from a reader zone by the presence of the mode writer. While the mode reader is optional and almost never specified for a reader zone due to its being the default mode, the mode writer is required to mark the entrance of a writer zone.

Process

The following behind-the-scene process occurs:
  1. The task puts itself in the mel writers' queue.
     
  2. In due course, the task moves up to the top position of the writers' queue. If the mel buffer is all full, the task will wait at this position, until an empty slot in the buffer opens up. Other writer tasks that come up later have to queue behind the task.
     
  3. If and when an empty slot is available, the task holds on to this slot, gets into the exclusive writer zone and does its Generate work. Note that this exclusivity is for writing only. If the mel buffer has values in the slots at the reading end, consumer tasks can still empty them using the normal mel read access. The task only holds off other writer tasks at the writing end -- which means that other writers must queue behind the task when it is in the writer zone, even if there are available empty mel buffers for them to use.
     
  4. When inside the writer zone, the task makes use of the store variable. This is an eponymous local variable whose initial value is undefined. The usual goal of a task inside the writer zone is to give a relevant value to the eponymous local variable.
     
  5. When the task is done with the writer zone, it either falls out of the writer zone's code at the closing }, or invokes the checkout operation. The default behavior in both cases is to copy the current value of the eponymous variable into the mel buffer slot that the task is holding. The filled slot is then freed for a reader task.

    If the task has not assigned a value to the eponymous variable, the skip behavior takes over. The held mel buffer slot remains empty, and is freed for a new writer task to deposit a value.
     
  6. The task removes itself from the mel writers' queue. Any writer task waiting on one of the writers' queues can now vie for the next empty slot.
Conditions

Let us repeat the conditions that allow the task to get into the writer zone:
  1. The task must be at the top position of the writers' mel queue
  2. The mel buffer must have an empty slot for the task to deposit a potential product
If one of the above conditions is not met, the task will be waiting in the writers' mel queue.

Usage

The Generator example can be written using a straight mel write instead of a mel write zone:
/* Generator - VERSION 2 */
void Generator(<mel> int store) {
    try {
      <?>store = Generate();
    }
    catch ( store<CLOSED> ) {}
}
In VERSION 2, the Generate functionality is done without holding up the mel store. Other writer tasks are free to write to the mel store while Generate is running. Exclusive access to the writer's end is requested only to transfer the Generate'd value to the mel variable. In VERSION 1, the Generate functionality is done inside the exclusive write zone, blocking other writer tasks to write to the mel store even if there are empty slots to receive their products. Which construct to use depends on the logic of the program.


Checkout Operation

A task can get off a writer zone by either falling off the closing } parenthesis or by explicitly invoking the checkout operation. The following version of the Generator is logically equivalent to VERSION 1:
/* Generator - VERSION 3 */
void Generator(<mel> int store) {
    try {
      <?writer>store {
          store = Generate();
          <checkout>;
      }
    }
    catch ( store<CLOSED> ) {}
}
The checkout operator, invoked without any attribute, implements the default behavior, which is for the task to copy the value of the eponymous variable into the top slot at the writer's end of the mel buffer, release that slot for a reader task, remove itself from the mel writer's queue and get off the exclusive writer zone.

Skip Behavior

There are times the logic dictates that copying the value of the eponymous variable to the mel slot is not desired. For example, the Generator task may not want to write to the mel if the Generated value is 0:
/* Generator - VERSION 4 */
void Generator(<mel> int store) {
    try {
      <?writer>store {
          store = Generate();
          if ( ! store ) <checkout skip>;
      }
    }
    catch ( store<CLOSED> ) {}
}
The <checkout skip> causes the task to check out of the writer zone; however the skip attribute prevents the value of the eponymous variable to be copied into the mel slot. The slot remains empty, and is available for the next writer to fill.

The skip behavior can also be used when the eponymous variable is not even present inside the writer zone. Let's introduce a new task called Maintenance whose purpose is the hold off the production line to do some maintenance:
/* Maintenance - VERSION 1 */
void Maintenance(<mel> int store) {
    try {
      <?writer>store {
          doMaintenance();
          <checkout skip>;
      }
    }
    catch ( store<CLOSED> ) {}
}
When Maintenane is in the writer zone, it locks the mel buffer at the writer's end, preventing any writer task to deposit a new product. These writer tasks can only resume depositing after Maintenance has checked out.

The skip attribute is used here to explicitly indicate that the mel buffer at the writer's end will not be updated. However for Maintenance, the skip attribute can be skipped (pun intended) since Maintenance does not update the eponymous variable store (it does not even make a reference to it!). When the eponymous variable is not initialized, the default behavior of checkout is skip:
/* Maintenance - VERSION 2 */
void Maintenance(<mel> int store) {
    try {
      <?writer>store {
          doMaintenance();
          <checkout>;
      }
    }
    catch ( store<CLOSED> ) {}
}
VERSION 2 behaves the same as VERSION 1. It is less verbose than VERSION 1 and also less explicit. Having the skip attribute visible as in VERSION 1 makes it clear the intended behavior of a checkout operation.

There is a third way to write the above Maintenance code. It is to use the mode attribute to specify the skip behavior as the default behavior.

Eponymous Local Variable

As introduced in Process, the eponymous local variable store has the same name as the mel variable store. Its initial value is undefined. The task then works with this local variable within the writer zone. On exit of the zone, if the eponymous local variable has a bona-fide value, this value will fill the mel buffer slot that the task is holding in the zone. This slot will be available for a reader task to empty. If the eponymous local variable remains undefined, then the task will release the holding empty mel buffer slot without filling it. The next incoming writer task can use that slot to deposit its value.

The eponymous local variable is not the same as the locally cached <value> property. The latter contains the mel value from the last time the task successfully reads from the mel. On the other hand, the eponymous local variable starts out as undefined, and is initialized inside the writer zone. If desired, the programmer can initialize the eponymous local variable with the mel <value> property:
store = store<value>;

Waiting Tasks Wake-Up Order

The previous discussion mentions mel buffers when referring to mel variables. To increase concurrency, a mel variable should be buffered, so that reading can be done on one end of the buffer and writing can be concurrently done at the other end. However by default, a NERWous C mel variable is not buffered; or looking in another way, it has a buffer of one slot.

When the task gets into the writer zone of an unbuffered mel variable, it not only holds off other writer tasks waiting to write, it also holds off any reader task waiting to read due to the mel variable being empty. If the task checks out of the writer zone with a value for the mel variable, the waiting reader task will run first to retrieve the newly deposited value and make the mel variable empty again. The writer task waiting at the top of the writers' queue is run next to deposit a new value. However if the task checks out of the writer zone without depositing a value to the unbuffered mel variable, the writer task at the top of the writers' queue is run first to deposit a value before the waiting reader task can run and empty it.


Writer OR Zones

We have seen reader OR zones. Now let's take a look on writer OR zones. Let's modify Generator to make use of two mel variables, and fill the first one that is available.
/* Generator - VERSION 5 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer as=store> (store1 || store2) {
          store = GenerateWithSeed(store<value>);
      }
    }
    catch ( store1<CLOSED> && store2<CLOSED> ) {}
}
This new Generator also makes use of the <value> property. It generates a new value for the chosen mel variable based on the last read value of that mel variable by the task.

Process

This is the behind-the-scene process:
  1. The task puts itself in store1 mel writers' queue. If it finds itself at the top position of store1 writers' queue and that mel buffer has an empty slot, the task holds on to this slot, gets into the writer zone with store1.
     
  2. If one of the above conditions for store1 is not met, the task puts itself in store2 mel writers' queue. If it finds itself at the top position of store2 writers' queue and that mel buffer has an empty slot, the task holds on to this slot, gets into the writer zone with store2.
     
  3. Otherwise, the task will wait on both writers' queues for the writer zone conditions to happen in one of the queues.
     
  4. On the first queue that satisfies the conditions (task first on the queue, mel buffer with empty slot), the task holds on to the found slot, removes itself from the other queue, and gets into the writer zone with that mel variable.
     
  5. When inside the writer zone, the task makes use of the store eponymous local variable, which can represent either store1 or store2 depending on which mel variable has been picked. The name "store" comes from the attribute as=store in the mel writer zone wait construct.
     
  6. The task also makes use of store<value> which is a totally different entity than the eponymous local variable store. The entity store<value> is the local <value> property that the task has cached from its last access from the reader's side of store1 or store2 (depending on which mel variable is picked). This value may be undefined if the task has never successfully read from that mel variable. The Generator version above assumes that GenerateWithSeed will know how to handle undefined arguments.
     
  7. When the task is done (at the closing } of the writer zone), if the task has a non-null value in the eponymous local variable store, this value is inserted into the mel buffer slot that the task is holding on. If GenerateWithSeed returns a null value to the eponymous variable, the eponymous variable is deemed undefined and the task simply releases the hold to the mel buffer slot without updating it. The task remembers which writers' queue it uses, and so removes itself from that queue.

As Attribute

The as attribute is usually needed for an OR list so that it can represent the mel variable that is selected on entry of the writer OR zone, as we see in the Generator example above.

Sometimes, the as attribute is not needed. Let's modify the Maintenance example to use writer OR zone:
/* Maintenance - VERSION 3 */
void Maintenance(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1 || store2) {
          doMaintenance();
      }
    }
    catch ( store1<CLOSED> && store2<CLOSED> ) {}
}
Since Maintenance does not make use of the eponymous local variable, the attribute as is not necessary in the mel zone wait statement.

Traverse Behavior

The behind-the-scene description above uncovers a subtle difference between
<?writer as=store> (store1 || store2)
and
<?writer as=store> (store2 || store1)
The first mel item in the OR list is checked first. If it is emptied constantly by a reader task, the mel write wait on that first item is likely successful and the writer OR zone is spent more with the first mel than with the second mel.

This issue can be resolved by using the random traverse behavior for OR writes:
/* Generator - VERSION 6 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer as=store> (store1 ||<random> store2) {
          store = GenerateWithSeed(store<value>);
      }
    }
    catch ( store1<CLOSED> && store2<CLOSED> ) {}
}


Writer LIST Zones

Unlike the writer OR zone which only requires either specified mel resource, the writer LIST zone allows the programmer to have a task entering a writer zone with all the requested mel resources.

Let us write a new version of Generator using a LIST wait, with a comma separated the mel variables in the wait statement:
/* Generator - VERSION 7 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1, store2) {
          store1 = GenerateWithSeed(store1<value>);
          store2 = GenerateWithSeed(store2<value>);
      }
    }
    catch ( store1<CLOSED> || store2<CLOSED> ) {}
}
This is the behind-the-scene process:
  1. The task puts itself in both the store1 and store2 mel writers' queues.
     
  2. In each queue, whenever the task finds itself at the top position and that mel buffer has an empty slot, the task holds on to this slot, and continues to check for the same write conditions on the other queue.
     
  3. When a task holds on to a slot in a queue, it will prevent other writer tasks waiting on that queue to proceed, even if there are empty slots to accomodate those writers. Since it is also waiting on another queue, a deadlock event can occur if multiple writer tasks vie for the same mel resources in a round-robin fashion.
     
  4. When the write conditions are also satisfied on the second queue, the task holds on the this slot. The task now holds on to both required slots, it can now check in to the writer zone.
     
  5. When inside the writer zone, the task makes use of the store1 and store2 eponymous local variables. These local variables start out as undefined, and then initialized with the result of the function call to GenerateWithSeed.
     
  6. The task also makes use of store1<value> and store2<value>, which are totally different entities than the eponymous local variable store1 and store2. The entity store1<value> and store2<value> are the local <value> properties that the task has cached from its last access to these mel variables from their reader's sides. These values may be undefined if the task has never successfully read from these mel variables. The Generator version above assumes that GenerateWithSeed will know how to handle undefined arguments.
     
  7. When the task is done (at the closing } of the writer zone), if the eponymous variable is non-null, its value is deposited into the corresponding empty slot that the task is holding on. If GenerateWithSeed returns a null value to the eponymous variable, the eponymous variable is deemed undefined and the task simply releases the hold to the mel buffer slot without updating it.

Writer LIST zones can be checked in and out without touching the eponymous local variables, as seen in this version of Maintenance:
/* Maintenance - VERSION 4 */
void Maintenance(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1, store2) { {
          doMaintenance();
      }
    }
    catch ( store1<CLOSED> || store2<CLOSED> ) {}
}
The task Maintenance will enter the writer zone holding on the mel empty slots of both store1 and store2. Upon exit of the zone, the task simply releases the empty slots it is holding without filling them with a value. Waiting or future writer tasks will fill in these slots. As discussed previously, one possible use of this type of Maintenance task is to block write access to the mel product lines in order for some maintenance work to take place.

The writer LIST zone should not be used if the programmer has knowledge of round-robin resources requests. These can cause deadlocks. For example, task 1 has hold to mel 1 and waits for mel 2, while task 2 has hold to mel 2 and waits for mel 1. If deadlocks are possible, writer AND zones should be considered.


Writer AND Zones

The writer AND zone should be used when there is a chance of requests for mel resources to cause deadlocks. The requested resources are listed with the && separator:
/* Generator - VERSION 8 */
void Generator(<mel> int store1, <mel> int store2) {
    try {
      <?writer> (store1 && store2) {{
          store1 = GenerateWithSeed(store1<value>);
          store2 = GenerateWithSeed(store2<value>);
      }
    }
    catch ( store1<CLOSED> || store2<CLOSED> ) {}
}
The writer AND zone is entered via the mel AND write waits applied to all the requested mel variables (store1 and store2 in the above example). This wait is an all-or-nothing wait. If the Generator task has the "top-of-the-queue" condition satisfied in one queue, but not at the other queue(s), it will allow other writer tasks at that queue to "jump the line" and put their products to the mel buffer empty slots. When the task belongs to the "top-of-the-queue" groups in all of the mel requests, then it will prevent later-coming writers to "jump the line". This allows the task to have exclusive access to the writer's end of the mel buffers of all the requested mel variables during its stay in the writer exclusive zone.

Inside the writer zone, the task makes use of the eponymous local variables that represent the requested mel variables. The processing on checkout of a writer AND zone is the same as for a writer LIST zone described previously.

Both the reader AND zone and the writer AND zone prevent deadlocks from multiple tasks that vie for the same resources in a circular way. Is the waiting problem more prevalent with the reader AND zone, since there is only one mel value at the top of the reading side of a mel buffer? At least at the writer's end, we can increase the buffer size so that multiple writers can deposit their products. However the waiting to enter a writer AND zone is as dire as waiting to enter a reader AND zone. This wait is not to access the available empty buffer slots (which we can increase), but to the top end of the writer's queue (which there is only one, like the top end of the reader's end queue). Once a task is in the writer end zone, it will block other writer tasks to access the writer's end of the queue, even though there may be empty slots for them to deposit their products.


Writer Zone Attributes

The attributes for a mel writer zone wait is similar to the ones for a mel reader zone:

AttributeSynopsis
modeDefault mode on checkouts
priorityWait to check in using this priority
timeoutWait to check in up to this timeout value. If the timeout is reached, a <TIMEOUT> exception will be raised.


Mode Attribute

The mode keyword is usually omitted. The following code snippets are equivalent:
<? mode=writer> store { ... }  /* canonical */
<?writer> store { ... }  /* popular */
There are two mode attributes for a mel writer zone wait:

Mode ValueSynopsis
writerAs a required value, it differentiates a mel writer zone wait from a mel reader zone wait. As the default behavior, it indicates that the checkout behavior is to update the held-up mel slot with the initialized value of the eponymous local variable. If the eponymous local variable is not defined or initialized, the checkout behavior is the skip behavior.
writer+skipThis sets the default checkout behavior to be the skip behavior, even if the eponymous local variable is defined and initialized.

The Maintenance example used in introducing the skip behavior on checkout, indicates that any checkout statement should be invoked with the skip attribute. Since the checkout behavior is always a skip, it can be specified on entry of the mel writer zone with the mode attribute:
/* Maintenance - VERSION 5 */
void Maintenance(<mel> int store) {
    try {
      <?writer+skip>store {
          doMaintenance();
          <checkout>;
      }
    }
    catch ( store<CLOSED> ) {}
}



Previous Next Top

Monday, September 5, 2016

Structured Mels

Welcome » NERWous C » Mel
  1. Mel Structures
  2. Mel Structure Buffers
  3. Mel Structures With Multiple Items
  4. Mel Structure Value Property
  5. Mel Structure Direct Itemized Access
  6. Mel Structures In Exclusive Zones
  7. Mel Structures Inside Local Structures


Mel Structures

In the previous section on Mel Arrays, we have the statement:
<mel> int stores[NUMITEMS];
where each element of stores is a mel element by itself and can be accessed independently. In this section, we are going to access stores as a single, monolithic mel element. Under NERWous C, this is done by using the mel struct construct:
#define NUMITEMS 3
struct s { int contents[NUMITEMS]; };
main () {
    <mel> struct s store;
    <!>Producer (store);
    <!>Consumer (store);
}
/* VERSION 1 */
void Producer (<mel> struct s store) {
    int c;
    struct s localstore;
 
    for (int i=0; i<NUMITEMS; ++i) {
       c = produce();
       if ( c == 0 ) {
          <close>store;
          return;
       }
       localstore.contents[i] = c;
     }
     
     <?>store = localstore;
}
/* VERSION 1 */
void Consumer (<mel> struct s store) {
    struct s localstore;
    try {
       localstore = <?>store;
       for (int i=0; i<NUMITEMS; ++i) {
            consume (localstore.contents[i];
       }
    }
    catch ( store<CLOSED> ) {
        printf ("Exit because the Store is closed");
        return;
    }
}
The C language built-in struct construct wraps around the contents array and allows the whole array to be accessed as a single mel element. Declaring a mel struct variable is a two-step process. First we define a regular C language struct template. Then we append the <mel> construct in order to declare the struct entity as a mel variable.

The Producer task uses a mel write to update the structured mel store. First it builds a local copy of store by running a serial for loop to populate the array contents with NUMITEMS items. If it detects a badly produce'd item, it prematurely closes the mel store. If all the products are good, it sends the whole local copy to the remote mel store, via the statement <?>store = localstore.

The Consumer task does a mel wait on the structured mel store. If during this wait it gets a CLOSED exception due to the Producer's premature closure, it jumps to the catch code block. When it sees that Producer has updated the mel store, it retrieves that value into its local copy via the mel read operation: localstore = <?>store. It then consumes that local copy with a serial for loop.


Mel Structure Buffers

The above Producer and Consumer process one structured mel and then close. Let's modify them so that they run continuously via an infinite while loop:
#define NUMITEMS 3
struct s { int contents[NUMITEMS]; };
main () {
    <mel buffer> struct s store;
    <!>Producer (store);
    <!>Consumer (store);
}
/* VERSION 2 */
void Producer (<mel> struct s store) {
   int c;
   struct s localstore;
   while ( 1 ) {
      for (int i=0; i<NUMITEMS; ++i) {
          c = produce();
          if ( c == 0 ) {
             <close>store;
             return;
          }
          localstore.contents[i] = c;
      }  
      <?>store = localstore;
   }
}
/* VERSION 2 */
void Consumer (<mel> struct s store) {
   struct s localstore;
   while ( 1 ) {
      try {
         localstore = <?>store;
         for (int i=0; i<NUMITEMS; ++i) {
            consume (localstore.contents[i];
         }
      }
      catch ( store<CLOSED> ) {
        printf ("Exit because the Store is closed");
        return;
      }
    }
}
We also add a buffer attribute to the structured mel declaration. The goal is to optimize the throughput of the program. If the Producer is faster than the Consumer, it can deposit a finished structured mel product into the writer's side of the buffer while the Consumer can take its time removing deposited structured mel values from the reader's side.

A buffer attribute without any argument means that the above example uses the default number of buffer slots as provisioned by the CHAOS run-time environment. We can specify a different number of slots in the buffer:
/* Increase the default buffer size by 5 more from default */
<mel buffer+5> struct s store;
/* Or specify a fixed number for buffer storage */
<mel buffer=25> struct s store;
Each slot in a structured mel buffer must contain the full structured mel. Naturally, to buffer or not to buffer a structured mel, and to pick the proper buffer size, depends on the need of the NERWous C program: a buffer can increase the performance of the program at the expense of increased resource cost.


Mel Structures With Multiple Items

A C structure can contain heterogeneous items. Likewise, a NERWous C structured mel can contain heterogeneous items, and these items go together from a concurrent access point of view.

Let's now enhance the previous example to introduce a numrounds item to the mel struct stores. Every time the Producer finishes one round of updating contents, it will increase numrounds by one. The Consumer displays the round number before consume'ing the contents:
#define NUMITEMS 3
struct s { 
   int contents[NUMITEMS]; 
   long numrounds;
};
main () {
   <mel buffer> struct s store;
   <!>Producer (store);
   <!>Consumer (store);
}
/* VERSION 3 */
void Producer (<mel> struct s store) {
   int c;
   struct s localstore;
   localstore.numrounds = 0;
   while ( 1 ) {
      for (int i=0; i<NUMITEMS; ++i) {
          c = produce();
          if ( c == 0 ) {
             <close>store;
             return;
          }
          localstore.contents[i] = c;
      }
      localstore.numrounds++;   
      <?>store = localstore;
   }
} 
/* VERSION 3 */
void Consumer (<mel> struct s store) {
    struct s localstore;
    while ( 1 ) {
       try {
          localstore = <?>store;
          for (int i=0; i<NUMITEMS; ++i) {
            consume (localstore.contents[i];
          }
          printf ("Consumed from round [%ld]", localstore.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
For both the Producer and Consumer, the numrounds value is accessed locally, either before the localstore is uploaded to the structured mel store, or after the structured mel is downloaded back to localstore.


Mel Structure Value Property

In the Consumer example above, we use a local variable, localstore, to capture the read value of the structured mel store. As we know about mel result properties, when a task does a read to a mel (be it a simple, array or structured entity as in this chapter), the read value of the mel is cached locally in the value property.

Let's replace localstore with the value property:
/* VERSION 4 */
void Consumer (<mel> struct s store) {
    while ( 1 ) {
       try {
          <?>store;
          for (int i=0; i<NUMITEMS; ++i) {
            consume (store<value>.contents[i]);
          }
          printf ("Consumed from round [%ld]", store<value>.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
Once the value is cached after the mel read, <?>store;, that value can be accessed via the value property, store<value>, like any other local variable. Since this is a structured mel, we use the dot (.) notation to get to the inner items (contents and numrounds).


Mel Structure Direct Itemized Access

The title of this section is a mouthful. What it means is that can we access an item of a mel structure directly over the NERW network, instead of downloading the whole mel structure and accessing that item locally, like we did using the localstore variable or the value property? The answer is no, but it may look like we can.

Let's re-write the Consumer task:
/* VERSION 5 */
void Consumer (<mel> struct s store) {
    while ( 1 ) {
       try {
           for (int i=0; i<NUMITEMS; ++i) {
             consume (<?>store.contents[i]);
          }
          printf ("Consumed from round [%ld]", <?>store.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
The above snippet is syntactically correct. However, under NERWOUS, the statement:
consume (<?>store.contents[i])
means:
<?>store; consume (store<value>.contents[i]);
In other words, the full structured mel is downloaded behind-the-scene, and then the requested element is presented from the local entity. For the for loop above, each iteration means a repeat of the wait on the mel structure and a full download, instead of a single wait and download for all iterations as in previous versions. Thus even though VERSION 5 code looks more compact, its run-time performance is much worse.

In addition, the integrity of the data consume'd is doubtful. If the Producer has generated another round of products between the Consumer for loop iterations, contents[i] and contents[i+1] may come from different rounds of the mel structure store.

Structured mel direct itemized access does have its use as a compact way to access a single item of a mel structure when we don't need to access other items. This method is bad here for our Consumer task because we also need to access other items of the mel structure. For this, the download-and-local-access method is the better approach.


Mel Structures In Exclusive Zones

Like any mel, structured mels can be accessed via mel zones: reader zones and writer zones.

Mel Structures In Reader Zones

Once a task is in a reader zone for a structured mel, it blocks all read access to that mel. If the mel is hosted remotely, it copies the contents of the structured mel into a local eponymous structured variable. It then works with this local variable. On the checkout off the reader zone, it can either removes the structured mel it is holding (causing the next reader task to pick up the next structured mel value), updates the remote structured mel without removing it (allowing the next reader to pick the same structured mel but with the updated value), or skips doing neither a removal nor update (letting the next reader to pick the same structured mel with the existing value).

In all cases, the task updates the local sequence property of the structured mel to be the same as the sequence number of the structured mel on entry of the reader zone. In the update and skip cases, this update prevents the task from processing a stale structured mel again. Upon encountering a stale mel, the task just waits for a new structured mel value to be deposited by a writer task.

We will now modify our Producer/Consumer example, so that the Consumer task will make use of the reader zone. In fact, we will have two Consumer tasks. The DO_EVEN one only processes the structured mel if the product's round count (numrounds) is even. Likewise the DO_ODD one processes only on odd numrounds:
#define NUMITEMS 3
#define DO_EVEN 0
#define DO_ODD 1
struct s { 
   int contents[NUMITEMS]; 
   long numrounds;
};
main () {
   <mel buffer> struct s store;
   <!>Producer (store);
   <!>Consumer (store, DO_EVEN);
   <!>Consumer (store, DO_ODD);
}
/* VERSION 3 */
void Producer (<mel> struct s store) {
   int c;
   struct s localstore;
   localstore.numrounds = 0;
   while ( 1 ) {
      for (int i=0; i<NUMITEMS; ++i) {
          c = produce();
          if ( c == 0 ) {
             <close>store;
             return;
          }
          localstore.contents[i] = c;
      }
      localstore.numrounds++;  
      <?>store = localstore;
   }
} 
/* VERSION 6 */
void Consumer (<mel> struct s store, int evenodd ) {
    while ( 1 ) {
       try {
          <?>store {
             if (  ((evenodd == DO_EVEN) && (store.numrounds % 2) == 0))
                || ((evenodd == DO_ODD) && (store.numrounds % 2) != 0)) ) {

                for (int i=0; i<NUMITEMS; ++i) {
                   consume (store.contents[i]);
         
                printf ("Consumed from round [%ld]", store.numrounds);
                <checkout>;   /* optional here */
             }
             else {
                printf ("Skipped round [%ld]", store.numrounds);
                <checkout skip>;    /* logically required */
             }
          }

          showme (store<value>.contents);
      }
      catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}

void showme (int contents[]) {
    printf ("Contents array:\n");
    for (int i=0; i<NUMITEMS; ++i) printf ("%d - ", contents[i]);
}
Within the exclusive zone, the task makes use the eponymous local variable, store, and thus does not need the wait operator ? as when it accesses the remote mel variable store.

When the DO_EVEN Consumer task sees an even numrounds, it will do the consumption of contents and invokes the <checktout> statement to get out of the exclusive zone. Although that statement is not required (since this is the default behavior when the code falls off the exclusive zone code block), it is added explicitly here to balance with the use of <checkout skip> in the else code block. If the numrounds is odd, the DO_EVEN Consumer task does not want to consume the contents; instead it invokes <checkout skip> to get out of the exclusive zone without reading out the structured mel. Then the DO_ODD Consumer enters the reader zone, it will pick up this value and consumes it.

The showme function is introduced as an artifact to discuss the value property of a mel variable on checkout of a reader zone. In both the <checkout> and <checkout skip> cases, the value property reflects the value of the eponymous local variable at the time of the reader zone checkout. For example, for the DO_EVEN task, if the numrounds is even, showme will show the contents that are copied from the structured mel to the eponymous structured variable, consume'd, and then copied from the eponymous structured variable to the value property; if the numrounds is odd, showme will show the contents that are are copied from the structured mel to the eponymous structured variable, and then copied from the eponymous structured variable to the value property, without being consume'd.

Mel Structures In Writer Zones

To illustrate the use of structured mels with a writer zone, we now change the Producer task:
/* VERSION 4 */
void Producer (<mel> struct s store) {
   int counter = 0;
   while ( 1 ) {
      <?>store = {
          for (int i=0; i<NUMITEMS; ++i) {
              store.contents[i] = produce();
              if ( store.contents[i] == 0 ) 
                  <checkout skip>;
          }
          store.numrounds = ++counter;
      } /* implicit <checkout> */
   }
} 
The new Producer waits until it is at the top of the mel store writer queue and an empty slot opens up. The Producer then puts a hold on that slot, and enters the writer zone. Inside the zone, the local eponymous variable store is declared by default and available for Producer to use. It then starts to fill the store.contents with produce results.

If a result of a produce is 0, the Producer gets out of the writer zone with a <checkout skip> statement: it releases the hold of the empty slot of the mel buffer without writing anything to it. The next writer task can make use of that slot. In our example here, there is no other writer task besides Producer. Since Producer runs an infinite while loop, it will iterate and gets into the writer zone again. Hopefully this time the issue that causes produce to return 0 has been resolved, and Producer can fill all the NUMITEMS of the local store.contents. It then updates the local store.numrounds, then gets out of the writer zone by doing an implicit <checkout> statement at the closing } of the zone code block.

When a task invokes an explicit or implicit <checkout> operation, the eponymous variable that the task has been building inside the writer zone, is written into the empty slot on hold. The task then frees the hold to allow a reader task to access the new structured mel value.

The big difference between VERSION 3 and VERSION 4 of Producer is the holding of the empty slot to receive the write. In VERSION 3, the Producer builds the items of the structured mel store locally. Once everything is all built, it waits for the empty slot from the writer side of the mel buffer, and gets hold of it long enough to copy the items over. The VERSION 4 of Producer does the reverse. It makes sure that it has an empty slot to hold on to before it starts to do the produce'ing and updating the numrounds value. The latter method keeps the hold on the empty slot much longer, potentially making other writers to wait on accessing the mel. On the other hand, if what it produces is precious, it is willing to trade concurrency performance for the assurance that it can deposit its product.


Mel Structures Inside Local Structures

In the previous sections, we include new items into a mel structure to make it a multi-item mel structure. We will now do the reverse: we will include a mel entity inside a local structure. Inside this local structured, whatever declared local can only be accessed by the declaring task, and whatever declared as a mel is accessible by other tasks.

In the following example, we define struct sn to include a "serial number" identification that is local to Producer which uses it to stamp an identification into each of its products. The produced items are still stored in the mel structure store which is shared between the Producer and the Consumer. However the Consumer does not have access to the "serial number" information used solely by the Producer.
#define NUMITEMS 3
struct s { 
     int contents[NUMITEMS]; 
     long numrounds;
};
struct sn {
 int snid;
 <mel> struct s store;
}
main () {
     struct sn localsn;
     localsn.snid = 8475;     /* starting serial number */
     localsn.<?>store = {1, 2, 3}, 0L); /* seed the store */

     <!>Producer (localsn);
     <!>Consumer (localsn.store);
}
/* VERSION 5 */
void Producer (struct sn localsn) {
   try {
      struct s localstore;
      localstore.numrounds = 0;
      while (1) {
         for (int i=0; i<NUMITEMS; ++i) {
            int c = produce(localsn.snid++);
            if ( c == 0 ) {
                <close>localsn.store;
                break;
             }
             localstore.contents[i] = c;
         }
         localstore.numrounds++;
         localsn.<?>store = localstore;
      }
   }
   catch ( localsn.store<NOREADERS> ) {
        printf ("Exit because no more Consumer");
        return;
   }
} 
/* VERSION 4 */
void Consumer (<mel> struct s store) {
    while ( 1 ) {
       try {
          <?>store;
          for (int i=0; i<NUMITEMS; ++i) {
            consume (store<value>.contents[i]);
          }
          printf ("Consumed from round [%ld]", store<value>.numrounds);
       }
       catch ( store<CLOSED> ) {
          printf ("Exit because the Store is closed");
          return;
       }
    }
}
We re-use VERSION 4 of the Consumer code. For the Producer, its argument is changed to the local struct localsn which contains the structured mel store. Producer accesses store via the construct:
localsn.<?>store
if outside a mel exclusive zone (like in Producer above), and via:
localsn.store
if inside an exclusive zone (as in the Consumer below).

What if we also pass localsn to Consumer? Let's look at this code:
/* VERSION 7 */
void Consumer (struct sn localsn) {
    printf ("First serial number [%d]", localsn.snid);
    try localsn.<?>store {
     printf ("Updated serial number [%d]", localsn.snid);
     printf ("Consuming from round [%ld]", localsn.store.numrounds);
        for (int i=0; i<NUMITEMS; ++i) {
            if (! consume (localsn.store.contents[i]) ) 
                return;      /* triggering the NOREADERS exception at Producer */
         }
         <resume>;     /* back to the mel zone wait for more consumption */
    }
    catch ( localsn.store<CLOSED> ) {
        printf ("Exit because the Store is closed");
        return;
    }
    catch ( localsn.store<NOWRITERS> ) {
        printf ("Exit because there is no more Producer");
        return;
    }
}
The "First" printf statement will show the value 8475 initialized by the task main at the time that it creates the task Consumer via the pel <!> statement. In each round going through its read zone, Consumer also does an "Updated" printf statement. This statement still shows the value for localsn.snid to be 8475, not whatever value Producer has at that moment. This is because locansn.snid is local to each task, and the above Consumer has not changed the value of this variable since accepting it from main. On the other hand, the localsn.store.contents and localsn.store.numrounds will reflect the new values from Producer since they are items of a structured mel.


Previous Next Top