Pages

Tuesday, January 23, 2018

Mel Writes

Welcome » NERWous C » Mel
  1. Mel Write Attributes
  2. Mel Writer Mode
  3. Mel Write Waits
    1. Mel Write Process
    2. Mel Write Conditions
    3. Mel Write Buffers
  4. Mel Writeover Mode
  5. Mel OR Writes
    1. Serial OR Writes
    2. Random OR Writes
    3. OR Write Exceptions
  6. Mel AND Writes
    1. Serial AND Writes
    2. Parallel AND Writes
    3. InSimul AND Writes
  7. Mel LIST Writes


Mel Write Attributes

The other side of a mel read which we explore in the previous chapter, is a mel write. Let's consider these two mel write statements:
<?>store1 = c1;
<? priority timeout mode="writeover">store2 = c2;
The first statement is a simple mel write statement with no attributes. The second statement is a mel write statement with all the attributes stated:

OperationAttributesSynopsis
<?>store = c Mel write operation on the mel variable store
priority
=n
Access the mel element for writing with priority n
timeout
=msec
Wait for the mel buffer to have an empty slot for writing for msec milliseconds before aborting
mode
=setting
Request write access to the mel with this mode setting:
writer: request a write access (default)
writeover: request an overwrite access
method
=<keyword>
Method used for mel multiple writes: mel OR writes and mel AND writes

A mel write statement is not only about accessing the remote mel element, but also about waiting for the task's turn to access in one of the writer's queues, then waiting for an empty slot in the mel buffer from the writer's end, and finally depositing the new value to that available slot. The last action will allow any reader task that is waiting for the mel element to be filled, to read out the new value and make the mel buffer slot available again for a new write.

Instead of depositing a new value, a writer task can overwrite the last written (and still un-read) value in the mel buffer, by using the mode attribute writeover. This feature can be used by a producing task to keep improving the interim result until a reader task takes it away.


Mel Writer Mode

A mel write operation accesses a mel variable at the writer's end of the mel buffer. A fully descriptive mel write operation specifies the writer mel buffer access mode. For example to write to the mel variable store the value of the local variable c:
<? mode=writer> store = c;
The mode keyword can always be omitted:
<?writer> store = c;
Finally, the attribute value writer can also be omitted:
<?> store = c;
Due to the position of the mel variable on the left-hand side of the = operator, a mel write operation can be identified at compile time without the verbose use of the writer mel buffer access mode. However, when we come upon mel writer zones in the future, we'll see the writer mode is necessary to differentiate them from mel reader zones.


Mel Write Waits

In the following Producer/Consumer example, the Producer task makes use of the mel write access:
main () {
  <mel> int store;
  <!> Producer (store);
  <!> Consumer (store);
}
void Producer (<mel> int store) {
  while ( 1 ) {
    try { <?>store = Produce(); }
    catch ( store<TIMEDOUT> ) {
      printf ("Timeout - try again");
      <resume>;
    }
    catch ( store<CLOSED> ) {
      printf ("Store closed - get out");
      break;
    }
    catch ( store<...> ) {
      printf ("Unexpected error [%d] due to [%s]", store<error>, store<why>);
      break;
    }
  }
}
void Consumer (<mel> int store) {
  int maxconsump = 500;
  while ( maxconsump-- ) {
    Consume(<?>store);
  }
  <close>store;
}
This Consumer task only Consumes 500 items. Then it ends the while loop, closes the mel store, and ends itself.

The purpose of the Producer task is to keep Produce'ing the items for Consumer until the latter is done with consumption.

Mel Write Process

This is the behind-the-scene process for the statement
<? timeout>store = Produce();
  1. The Producer task Produces a new product and puts itself into one of many possible writers' queues of the mel store.
     
  2. The writers' queue Producer selects to stand in line is based on the priority of the mel write operation. Here, no priority attribute is explicit so the default writers' queue is used.
     
  3. If there are other writer tasks in the queue, the Producer task moves up the queue as the front tasks get off the queue, until it reaches the top position of the queue.
     
  4. At the top position, the Producer waits for the queue to have WRITE access to the mel store. In this example, Producer is the only writer task, there is no vying for the access from higher priority queues. Therefore, although the default writers' queue has the lowest priority to write to the mel buffer, it is granted WRITE access.
     
  5. If the mel buffer is vacant, the Producer task will deposit its product into the mel buffer from the writer's end, increases the sequence number, and gets off the mel store default writers' queue. The mel buffer is now filled. The slot with the new product eventually moves up the mel buffer to the top position on the reader's end and becomes available for retrieval by the Consumer reader task.
     
  6. If the mel buffer is fullProducer keeps the top position in the queue and waits until the mel buffer of store becomes vaant. Then it processes as above.
     
  7. If Producer cannot write its product before the timeout, the TIMEDOUT exception is raised. Producer will get off the mel store writers' queue, and processes the <resume> operation to try to deposit again.
    If we were using continue instead of <resume>, we would loop back the while loop, which would cause a new product to be Produced. The product that got timed out would be lost. If the logic of the program were to skip any timed out item (perhaps because it had become obsolete), then continue would be the right choice.
  8. If Consumer has processed 500 items, it will close the mel element store. On the next iteration of the while loop, the Producer will get hit with the CLOSED exception, when it tries to write to the mel store.
     
  9. When the Producer receives the CLOSED exception, it will get off the mel store writers' queue, and processes the CLOSED catch handler. Here, it breaks out of the while loop, and with nothing else to do, it will end itself.
If during an attempt to write to the mel store, the Producer detects an exception other than TIMEDOUT or CLOSED, it will get off the mel writers' queue, and jumps to the catch-all handler:
catch ( store<...> )
Here, it prints the Unexpected exception statement with the error code and cause of the exception, and then breaks off the while loop. Without the catch-all handler for store, any unexpected and thus unprocessed exception from store will cause the Producer task to abort abruptly, instead of ending graciously.

Mel Write Conditions

As a summary, the following conditions must be met before a task can write in a mel value:
  1. The task is at the top position of a writers' queue
  2. The writers' queue has been granted WRITE access to the mel element
  3. The mel element must have a vacant slot for the task to deposit its product
The first condition is relevant when there are other tasks already in the queue. Only the task at the top of the queue can deposit its product to the next available slot of the mel buffer from the writer's end.

The second condition is relevant when there are multiple queues vying for access to the same mel element. A queue with a higher priority is granted access to the mel element more often than lower priority queues.

The third condition is relevant when the mel element is full. The writer task must wait for a reader task to remove a value in the mel buffer to make room for the deposit.

Mel Write Buffers

The purpose of a mel write request is to deposit a new value to the mel element. If the mel element is not buffered -- which is the default for a mel declaration, the writer task has to wait for a reader task to empty the mel element to make room. If the mel element is buffered, the writer task can deposit into an empty slot in the buffer until the buffer is full.

The availability of a mel buffer differentiates a mel write from a mel read. A read can only work with only the top value of a mel. This also how a write works if the mel is not buffered. If a programmer buffers a mel variable, a writer can deposit in the empty slots of the buffer without waiting for a reader to empty the top slot. With enough resources assigned to the buffer, a mel write wait can be greatly minimized.
Note that we have been using the term mel buffer even on mel elements that are not buffered. In these cases the mel buffer is considered to be of size one (i.e. having one slot).


Mel Writeover Mode

In addition to the default writer mode which requires the task to wait for a vacant mel buffer, the mel write operation supports the writeover mode that does not require such wait. This mode behaves like the writer mode if the mel buffer is vacant, but if the buffer is full or filled, it will overwrite the last written item without looking for an empty slot.

Let's create a new Producer that creates a "good-enough" product first for impatient consumers. It then produces a "better" product to replace the "good-enough" product if the latter has not been retrieved by any consumer.
void Producer (<mel> int store) {
  int c;
  while ( 1 ) {
    if ( c = ProduceGood() ) break;
    <?>store = c;
    if ( c = ProduceBetter() ) break;
    <? mode=writeover>store = c;
  }
  <close>store;
}
The mode keyword is optional. The Producer could also make this statement:
<?writeover>store = c;

The above Producer makes two write operations to the mel variable store. The first one is a normal write operation using the default writer mode where it has to wait for a vacant slot in the mel buffer. The second write overwrites the value from the first write if it has not been picked up by a consumer; otherwise it just deposits the "better" product for the next consumer.

This is the behind-the-scene processing of a generic task doing a writeover to a mel element. The first steps are the same as those for a writer mode:
  1. The task generates a new product and puts itself into one of many possible writers' queues of the mel element.
     
  2. The writers' queue the task selects to stand in line is based on the priority of the mel write operation. If no priority attribute is explicit, the task joins the default writers' queue.
     
  3. If there are other writer tasks in the queue, the task stays in the queue, and moves up to the front of the queue as the tasks in front of it get off the queue.
     
  4. At the front position, the task waits for the queue to have WRITE access to the mel buffer due to competition from higher priority queues, if any.
     
  5. After its queue is granted WRITE access, the task checks for the status of the mel buffer. This is where the writeovr mode can differ from the default writer mode:
    • If the mel buffer is empty (i.e., all mel buffer slots are vacant), the task deposits its product at the writer's end of the mel buffer. This is the same as in a writer mode.
       
    • If the mel buffer filled (i.e., some slots are vacant and some not), the task overwrites the last written slot with its own product. This differs from the writer mode where the task deposits its product on an available vacant slot.
       
    • If the mel buffer is full (i.e., none of the slots are vacant), the task overwrites the last written slot with its own product. This differs from the writer mode where the task waits for a vacant slot to be made available.
  6. The task increases the sequence number of the deposited or overwritten item, and gets off the mel queue with no error reported.
     
  7. If there is an error during the writeover process, the task will get off the mel queue without making any new deposit or overwrite in the mel buffer, and raises an exception.
The above Producer attempts to overwrite a "good-enough" product with a "better" product, but a fast Consumer task can still retrieve the "good-enough" product before Producer has time to overwrite it. In addition, if there is another writer task that inserts its own product right after Producer writes in its "good-enough" product, the value that Producer overwrites with its "better" product will be of the other writer task since this value is the last written value.

There are several uses of a writeover mode in NERWous C:
  1. Ad-hoc use like in the above example, to fit the logical need of a program
  2. As the write mode for a read-only mel
  3. As the write mode for a release statement
  4. As the write mode for exclusive zones


Mel OR Writes

Like the mel OR read, there is a corresponding mel OR write. Let's consider a Producer task that can send its product to either store1 or store2 mel elements, depending on which one is available to receive the new value. There are two methods that such a task can use to traverse the mel elements:

Mel OR Write Methods
Method ValueDescription
SerialThe task checks the mel elements serially based on the order in the OR list.
RandomThis is the default method when no method is specified. A random generator dictates which mel element in the OR list is checked first. The task then circulates through the remaining mel elements in the list. This method is more compute intensive but gives a fairer chance to all mel elements in the OR list.


Serial OR Writes

Let's start with a Producer that uses the Serial method:
void Producer (<mel> int store1, <mel> int store2) {
  while ( <? method=serial>(store1 || store2) = Produce() );
  <close>store1;
  <close>store2;
}
The keyword method can be omitted:
while ( <? serial>(store1 || store2) = Produce() );
This is the behind-the-scene process of the Serial mel OR write statement:
<? serial>(store1 || store2) = Produce()
  1. The Producer task Produces a product, and puts itself onto store1 default writers' queue.
     
  2. If the mel WRITE conditions are satisfied on store1, Producer will deposit its product into store1 buffer at the writer's end, increases the sequence number on store1, gets off the mel store1 default writers' queue, and exits the mel OR write statement. The store1 mel element is now filled for a reader task.
     
  3. If the mel WRITE conditions are not met at store1, the Producer continues to wait in the store1 writers' queue, and also puts itself to store2 default writers' queue.
     
  4. If the mel WRITE conditions are satisfied on store2, Producer will deposit its product into store2 buffer at the writer's end, increases the sequence number on store2, removes itself from both mel writers' queues, and gets off the mel OR write statement. The store2 mel element is now filled for a reader task.
     
  5. Otherwise, Producer will wait on both queues, until the mel WRITE conditions become true for either one. The Producer then deposits its product to the identified mel buffer, increases the sequence number on that mel element, removes itself from both queues, and gets off the mel OR write statement. Either store1 or store2 has been filled, but Producer does not know which one.
     
  6. If it happens that the mel WRITE conditions become true at both mel elements at the same time, the CHAOS runtime will ensure that Producer will write to one or the other mel element , and not both.
In the above example, Producer keeps producing and depositing its product to either store1 or store2 whichever is vacant at that time, until it Produces a zero value. It then breaks off the while loop and closes both mel channels.

Random OR Writes

Although either mel element can be selected, there is a bias for the first mel element in the Serial OR write method, due to it being looked on first when the OR write starts. If this first mel element is always vacant, perhaps due to it having an ample buffer or working with a fast reader task, it will keep being picked up, shunting the rest from ever be. If the logic of the program demands that all mel elements in the mel OR write have a fair chance, then the Random method is used:
void Producer (<mel> int store1, <mel> int store2) {
  while ( <? method=random>(store1 || store2) = Produce() );
  <close>store1;
  <close>store2;
}
The keyword method can be omitted:
<? random>(store1 || store2) = Produce()
Since the Random method is the default method for mel OR writes, the keyword value random can also be omitted:
<?>(store1 || store2) = Produce()
The behind-the-scene process for the Random mel OR write starts with the task picking a mel element in the mel OR list in a "fairly random" manner. NERWous C does not define a particular strategy for "fairly random" as long all the mel elements in the mel OR list has a fair chance to be picked first.

Once the starting element is picked, the Random method follows the Serial method. After the last mel element in the OR list is checked and found not available for writing, the Random method wraps around and checks the first element in the mel OR write list.

OR Write Exceptions

The above Producer assumes no error to happen in the mel OR write operation. The following Producer is hardened with exceptions to handle possible errors:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      int c = Produce();
      if ( !c ) break;
      <? timeout=100>(store1 || store2) = c );
    }
    catch ( (store1 || store2)<TIMEDOUT as=store> ) {
      printf ("The mel element [%s] has timed out", store<name>);
      <resume>;
    }
    catch ( (store1 && store2)<CLOSED> ) {
      printf ("Both mel elements have closed");
      break;
    }
    catch ( (store1 || store2)<... as=store> ) {
      printf ("The mel element [%s] has error [%d]", store<name>, store<error>);
      if ( store<error> == NERW_ERROR_CLOSED ) <resume>;
      break;
    }

    <close>store1;
    <close>store2;
}
The exception rules for mel OR writes are similar to the mel OR reads rules:
  • Skipping rule: if an exception happens to a mel element, and there is no specific handler for that mel exception, the CHAOS runtime will remove the task from waiting on that mel element. The mel OR write continues with the mel elements that have not had any exception.
     
  • Aborting rule: if there is a specific handler for that mel exception, the mel OR write is aborted. The task gets off all the waiting mel queues, including those that belong to mel elements that do not have exceptions; and the task will process the identified handler.
     
  • Precedence rule: individual exception handlers take precedence over combination exception handlers. An AND combination exception handler takes precedence over an OR combination exception handler.
Based on the above rules, we can simplify Producer as follows:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      <? timeout=100>(store1 || store2) = Produce() );
    }
    catch ( (store1 && store2)<...> ) {
      if ( store1<error> == NERW_ERROR_TIMEDOUT || store2<error> == NERW_ERROR_TIMEDOUT)
          <resume>;
      break;
    }
  }
  <close>store1;
  <close>store2;
}
In this new Producer, if one mel element gets into an error, it will be automatically skipped; and the mel OR write continues with the other mel element. There is no need for an explicit handler for this built-in behavior.

If both mel elements get any kind of error (same or different errors), then the task gets off the mel OR write, and processes the AND combination catch-all exception handler. If one or both errors are a TIMEDOUT, the task does a resumption in order to try to deposit the current product from Produce again. The timed-out mel element or elements will be cleared on this error on exit of the handler, and can accept a write deposit. (If the logic of the program says to discard timed-out products because they are deemed "stale", the continue statement should be used to re-iterate the while loop for a new product, instead of the <resume> statement.)

If neither mel elements get a TIMEDOUT error (i.e. both get a CLOSED or some other error), the task breaks off the infinite while loop. The <close> operations that Producer does on the mel elements indicate to the other tasks in the program that Producer is done with the mel elements. These operations are extraneous if the mel elemnents are already closed by other tasks.

Mel AND Writes

While the mel OR writes allow writing into one of the listed mel elements, the mel AND writes are for writing to all of them. The AND mel write has several built-in methods to process the listed mel elements:
Mel AND Write Methods
Method ValueDescription
SerialThe task writes to the mel elements serially based on the order in the AND list.
ParallelThis is the default method when no method is specified. The task makes a parallel attempt to write to all the mel elements in the AND list. The writes occur individually whenever a mel element is available for writing by this task.
InSimulThe task makes a parallel attempt to write to all the mel elements in the AND list. The writes occur simultaneously when all mel elements are available for writing by this task.


Serial AND Writes

To illustrate the mel AND write Serial method, let's first replace the Produce function with ProduceTwo which returns two values:
void Producer (<mel> int store1, <mel> int store2) {
  int c1, c2;
  while ( 1 ) {
    (c1, c2) = ProduceTwo();
    <? timeout> store1 = c1;
    <? timeout> store2 = c2;

    if ( !c1 || !c2 ) break;
  }
  <close>store1;
  <close>store2;
}
Once ProduceTwo generates the two products, they are stored in the local variables c1 and c2. The Producer task then writes the first product to the mel buffer store1. Once that successful, it writes the second product to the mel buffer store2. After both writes are done, Producer checks if any product is 0 which marks the end of the production; in that case, it breaks out of the while loop and lt;close> the mel buffers to signal to any reader tasks the end of production.

The above Producer code is now simplified with the mel AND write statement:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      <? timeout serial>(store1 && store2) = ProduceTwo() );

      if ( !store1<value> || !store2<value> ) break;
    }
    catch (store1 || store2)<TIMEDOUT as=store>) {
      printf ("Can't put into %s in time", store<name>);
      <resume> store;
    }
    catch (store1 || store2)<CLOSED as=store>) {
      printf ("%s has closed", store<name>);
      break;
    }
  }
  <close>store1;
  <close>store2;
}
The optional keyword method can be used if one desires to be explicit:
<? timeout method=serial>(store1 && store2) = ProduceTwo() );
Let's look at the behind-the-scene process of the Serial method for a mel AND write:
  1. The Producer task runs ProduceTwo and generates two new items.
     
  2. The Producer task puts itself to store1 default writers' queue with the default timeout value.
     
  3. If the Producer cannot put its item into store1 buffer before the timeout, it aborts the mel wait on store1, exits the mel AND write, and triggers the TIMEDOUT exception on store1.

    The handler
    catch ( (store1 || store2)<TIMEDOUT as=store> )
    will pick up this exception. It printfs out a message then invokes <resume> to put itself back at the bottom of the store1 writers' queue to try to deposit the first produced item again.
     
  4. If the mel WRITE conditions are met at store1 before the timeout, the Producer deposits the first produced item into the mel buffer, and drops out from that mel writers' queue. This allows other writer tasks to write to the mel element store1.
     
  5. The Producer task now puts itself to store2 default writers' queue with the default timeout value.
     
  6. If the Producer cannot put the second produced item into store2 buffer before the timeout, it aborts the mel wait on store2, exits the mel AND wait, and triggers the TIMEDOUT exception on store2.

    The handler
    catch ( (store1 || store2)<TIMEDOUT as=store> )
    picks up this exception. It printfs out a message then invokes <resume> operation. This operation knows that the first produced item has been successfully deposited in store1. It therefore puts the Producer task back to the store2 writers' queue (at the bottom position) to try to deposit the second produced item again.
     
  7. If the mel WRITE conditions are met at store2 before the timeout, the Producer deposits the second produced item into the store2 mel buffer, and drops out from that mel writers' queue. This allows other writer tasks to write to the mel element store2.
     
  8. The Producer task exits the mel AND write statement with both produced items deposited into the corresponding mel elements, one after the other as dictated by the Serial AND write method.
     
  9. If the Producer task encounters a mel closure from any mel element during the mel waits, the CLOSED exception will be raised, and handled by:
    catch ( (store1 || store2)<CLOSED as=store> )
    This exception handler does a printf on the closing mel element and breaks out of the while loop.

    What would happen if a <resume> or continue were used instead of the break? The <resume> operation would clear all exceptions, including the CLOSED exceptions, retry the mel AND write on produced item that have not been written by sending a request to the remote mel elemnt(s), get back the piggy-backed status that the mel elements have been closed, jump into the CLOSED exception handler again, and keep repeating the same process, causing the Producer task to livelock -- i.e. to spin endlessly on the same produced items.

    The continue would also cause a livelock, but in a different way. This statement would not clear the exceptions, then cause the Producer to loop back to the while loop, ProduceTwo two new items, attempt to write them to the closed mel elements, detect locally that the mel elements already have the CLOSED status, replace the sending a write request to the remote mel elements with the jump into the CLOSED exception handler. This spinning is then repeated endlessly, each time producing and discarding two new products.
In summary, the Serial method for mel AND writes is just a more compact way to write the one-at-a-time program.


Parallel AND Writes

Let's now change Producer again so that it still add the products of ProduceTwo to both mel buffers, but we will remove the requirement that the additions to be done at the same time. Instead, they will be added at the same time. This AND write method is therefore referred to as the Parallel method:
void Producer (<mel> int store1, <mel> int store2) {
  while ( 1 ) {
    try {
      <?>(store1 && store2) = ProduceTwo() );

      if ( !store1<value> || !store2<value> ) break;
    }
    catch (store1 || store2)<TIMEDOUT as=store>) {
      printf ("Can't put into %s in time", store<name>);
      <resume> store;
    }
    catch (store1 || store2)<CLOSED as=store>) {
      printf ("%s has closed", store<name>);
      break;
    }
  }
  <close>store1;
  <close>store2;
}
The Parallel method is the default method for the mel AND write, and is assumed when no method is requested, as in the example above. A programmer can choose to be explicit:
<? timeout method=parallel>(store1 && store2) = ProduceTwo() );
Like always, the keyword method can be omitted:
<? timeout parallel>(store1 && store2) = ProduceTwo() );
This is the behind-the-scene processing for a mel AND write with the Parallel method:
  1. The Producer task runs ProduceTwo and generates two new items.
     
  2. The Producer task puts itself to both store1 and store2 writers' queues.
     
  3. Whenever the mel WRITE conditions are satisfied for one mel element, Producer will deposit the corresponding product into that mel buffer, and removes itself from that mel writers' queue.
     
  4. Whenever the mel WRITE conditions are satisfied on the remaining mel element, Producer will do the same thing as above for this mel element.
     
  5. If the mel WRITE conditions are not satisfied in time for one of the mel queues, the Producer task will get off the mel queue, and jumps into the handler
    catch ( (store1 || store2)<TIMEDOUT as=store> )
    It printfs out a message then invokes <resume> on that mel element. This operation will clear the exception on that mel element, puts the Producer task back at the bottom of that writers' queue to attempt to deposit the correspondent product again.
     
  6. Once the Producer task has both products successfully written into their corresponding mel buffers, it gets off the mel AND write statement.
     
  7. If while waiting in one mel element queue, the Producer task may get timed out in one (or both)
The randomness with the mel LIST write is that we don't know which mel buffer is written to first. If the order is important, then the Producer code must be explicit:
void Producer (<mel> int store1, <mel> int store2) {
     int c1, c2;
     while ( 1 ) {
          if ( (c1, c2) = ProduceTwo() ) break;
          /* Write to store1 then store2 */
          <?>store1 = c1;
          <?>store2 = c2;
     }
     <close>store1;
     <close>store2;
}



InSimul AND Writes

A mel AND write is used when a programmer wants both store1 and store2 to be written to at the same time. Let's now change Producer so that it calls a function ProduceTwo which returns two values. These values will be respectively inserted to store1 and store2 mel buffers at the same time.
void Producer (<mel> int store1, <mel> int store2) {
     while ( <?>(store1 && store2) = ProduceTwo() );
     <close>store1;
     <close>store2;
}
For example, we can think of a gaming program where a launching of nuclear missile requires a squeeze on a trigger and a press of a button to be received simultaneously. Of course for this program to work, the mel variables that represent the trigger and the button ought to be of buffer size 1, since it would be meaningless to buffer such requests.

The mel AND write operation behaves similarly to a mel AND read to prevent deadlocks in case of multiple tasks accessing the same mels for writing. The difference is that instead of waiting for a mel to be valued, a writer task waits for a mel to have an available slot at the mel buffer writer's end so it can deposit its product into. If the buffer is of size greater than 1, it is easier to satisfy a mel AND write than a mel AND read.

This is the behind-the-scene processing for a mel AND write:
  1. The task puts itself in both mel writer's queues.
     
  2. In due course, the task will be at the first position of one the mel writers' queues and joins the "top-of-the-queue" writers group of that queue. It then checks if it is also in the "top-of-the-queue" group at the other mel writer's queue. If it is not, it will wait for it to join the "top-of-the-queue" group at the other writers' queue.
     
  3. While waiting in only one of the "top-of-the-queue" group, if another writer task comes along on that writers' queue, the task will allow that later-coming task to "jump the line" and deposit its value to an available empty buffer slot if there is one.
     
  4. Eventually the task will be in the "top-of-the-queue" groups at both mel writers' queues. It will keep checking for empty slots at both mel buffers. If none is available, it will wait. If only one buffer is available, it will wait, but this time, it will not allow any later-coming writer task, that is not in the "top-of-the-queue" group, to "jump the line" and fill that available buffer slot.
     
  5. Eventually the task will find empty slots at both mel buffers. It will then fill the mel buffer slots with its products, increases the sequence number of both mel variables, removes itself from both writers' queues, and gets off the mel AND write.

There can be more than one task in a "top-of-the-queue" group. This group is reserved for tasks that are doing AND writes. When a task doing an AND write moves up to the first position in a writers' queue, it may not find all the conditions for a successful AND write at that moment. Instead of relinquishing the first position in the queue, it joins the "top-of-the-queue" group, and bides its wait there.

For example, we have a task T1 that does an AND write on mel resource R1 and R2. T1 is in the "top-of-the-queue" group on R2 but not yet on R1. Then a task T2 comes along and does a normal write on R2. If there is mel buffer slot available, T1 will allow T2 to "jump the line" and fills that slot. Eventually, T1 is in the "top-of-the-queue" groups of both R1 and R2. However one or both mel buffers are still full, so T1 waits. When the cyclic T2 comes back for another product deposit run, it cannot "jump the line" this time. It has to wait. The next available empty slot is already reserved to T1 since T1 has achieved the "top-of-the-queue" condition at both queues, and now needs the "available mel buffer slot" condition in order to fulfill its AND write.

Let's keep the above example but add a task T3 that does an AND write on resource R2 and R3. When T3 gets to the first position on R2 but not on R3, it will join T1 in the "top-of-the-queue" group of R2. Now if R2 becomes available, the CHAOS runtime automatically assigns it to T1 and not T3 since the former has attained "top-of-the-queue" in all the mels in the AND list, and not the latter.

Eventually T3 also joins the "top-of-the-queue" group of R3, and has achieved "top-of-the-queue" in all the mels in its AND list. Now if R2 becomes available, CHAOS will also give that slot to T1 because it has joined the "top-of-the-queue" group of R2 before T3. T1 now can satisfy all its AND write conditions, deposits its products and gets off both writers' queues. T3 has to wait for the next empty slot from R2 to go with the R3 slot it already has, before it can exit its AND write. The task T2 that is waiting for both T1 and T3 to be done with R2, can now access the next empty slot from R2.


Previous Next Top

Wednesday, January 17, 2018

A Tour Of Go Language

Welcome » NERWous C » Examples
  1. Go Concurrency
  2. Fibonacci Example
  3. Mutex Example


Go Concurrency

The Go language is an open-source programming language created by Google in 2009. It supports concurrency via Goroutines and channels. The NERWous C equivalents are pel constructs, and mel entities.


Fibonacci Example

The Fibonacci sequence appears frequently in mathematics, computer algorithms, and biology settings.

Go Implementation:

The Go language Fibonacci example displays the first 10 Fibonacci numbers (i.e. 0, 1, 1, 2, 3, 5, 8, 13, 21,34). This implementation is copied from the Tour Of Go tutorial:
package main
import (
 "fmt"
)
func fibonacci(n int, c chan int) {
 x, y := 0, 1
 for i := 0; i < n; i++ {
  c <- x
  x, y = y, x+y
 }
 close(c)
}

func main() {
 c := make(chan int, 10)
 go fibonacci(cap(c), c)
 for i := range c {
  fmt.Println(i)
 }
}
The Go example has main creating the channel c then forking a light-threaded task via the go command to run the fibonacci function in concurrency. The fibonacci task feeds the channel c whenever it has computed the next number for the sequence. The main task waits on the channel c for any new value to show it on the console via the Println command.

NERWous C Implementations:

We will do several versions of the NERWous C implementations to illustrate different features of the language.

Version 1:
Version 1 follows closely the Go implementation by replacing the Goroutine and Go channel with the pel and mel constructs of NERWous C:
/* VERSION 1 */
void main () {
  <mel> int c;
  <!> fibonacci (10, c);
  for (int i=0; i<10; ++i)
    printf ("%d", <?>c);
}
int fibonacci (int n, mel int c) {
  int x = 0, y = 1;
  for (int i = 0; i < n; ++i ) {
    <?>c = x;
    x = y; y = x + y;
  }
  <close>c;
}
The mel variable c is shared between the main task and the fibonacci task. The main task is the reader and the fibonacci the writer. When main tries to read out from the mel c and the fibonacci has not generated a value for it, main will wait at the mel wait operator <?>. When the mel channel c is filled, main reads out that value and makes the mel c empty again.

The fibonacci task needs the mel channel c to be empty so that it can deposit the x value. If the reader main is slow in removing the previous value, the fibonacci writer has to wait at the mel wait operator <?>.

Although the main and fibonacci tasks run in concurrency, they work in lock step due to the synchronizing chock point at the mel channel c. To possibly increase the concurrency, we can add buffering to the mel c:
<mel buffer=10> int c;

Version 2:
In the second version, we will drop the mel variable c and use the return value of the pel task as the communication channel.
/* VERSION 2 */
void main () {
  <pel> p = <!>fibonacci (10);
  for (int i=0; i<10; ++i)
    printf ("%d", <?>p);
}
int fibonacci (int n) {
  int x = 0, y = 1;
  for (int i = 0; i < n; ++i ) {
    <release> x;
    x = y; y = x + y;
  }
}
The return value of a pel task is a read-only mel. Via the <release> statement, the pelled task "streams" values to any task that is waiting on that return value channel. Unlike Version 1 where the main task depends on the mel entity to be filled, the main task in Version 2 uses the sequence number to make sure it does not process stale values.

Version 3:
The third version builds on the second version by having the main task keeps checking the released value of the fibonacci task until this task ends itself.
/* VERSION 3 */
void main () {
  <pel> p = <!> fibonacci (10);
  while ( 1 ) {
    try { printf ("%d", <?>p); }
    catch (p<ENDED>) break;
  }
}
int fibonacci (int n) {
  int x = 0, y = 1;
  for (int i = 0; i < n; ++i ) {
    <release> x;
    x = y; y = x + y;
  }
}
When the fibonacci task runs off its last statement, it will end itself. This triggers the ENDED exception at the main task which uses it to break out of the infinite while ( 1 ) loop.


Mutex Example

The Go language support the concept of a mutex for mutual exclusive access to shared data. A Go language block of code can be executed in mutual exclusion by being surrounding with a Lock and Unlock calls.

Go Implementation:

The Go implementation is taken from Tour of Go tutorial:
package main

import (
 "fmt"
 "sync"
 "time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
 v   map[string]int
 mux sync.Mutex
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
 c.mux.Lock()
 // Lock so only one goroutine at a time can access the map c.v.
 c.v[key]++
 c.mux.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
 c.mux.Lock()
 // Lock so only one goroutine at a time can access the map c.v.
 defer c.mux.Unlock()
 return c.v[key]
}

func main() {
 c := SafeCounter{v: make(map[string]int)}
 for i := 0; i < 1000; i++ {
  go c.Inc("somekey")
 }

 time.Sleep(time.Second)
 fmt.Println(c.Value("somekey"))
}
NERWous C Implementations:

NERWous C is the NERW Concurrency Model grafted on the C language, and the C language does not have native support for map nor string. The following NERWous C language implementations make use of a simple array with integer as index. This will not diminish the discussion on Mutex which is the focus here.

Version 1:
/* VERSION 1 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void Inc (int somekey) {
   <?>SafeCounter[somekey] {
      SafeCounter[somekey]++;
      <checkout replace>;
   }
}

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i) {
      <!>Inc (somekey);
   }
   sleep (1000);
   printf ("%d", <?>SafeCounter[somekey]);
}
In the first version, we first declare SafeCounter as a global mel array that can contain NUMKEYS items. The value of NUMKEYS come from the include file keys.h.

The main task runs a serial loop to initialize to 0 each of the item of SafeCounter. It then generates a random number between 0 inclusive and NUMKEYS exclusive via a custom function random (which we do not show the implementation). The main task then runs a second serial loop for 1000 iterations. In each iteration, it forks a new task via the pel construct to run the Inc function in parallel.

Inside the Inc function, the deceptively simple auto-increment means a read of the value of the mel entity SafeCounter[somekey], increment that value, and write that value back to the mel entity. Since the read and write must be atomic, all the mentioned operations must be inside a mel reader zone for mutual exclusion. When an Inc task is in the exclusive zone, all the other Inc tasks that needs access to the same SafeCounter[somekey] entity, must queue up in that entity's queue.

The first Inc task that comes to the reader zone can see that the conditions to get into the zone are satisfied. These conditions are the mel entity is valued and that value is not stale. These conditions are made satisfactory by the main task when it initializes all the items in the SafeCounter array to zero.

After doing the auto-increment using the local eponymous variable SafeCount[somekey], the Inc task writes back that value to the shared mel entity via the <checkout replace> operation. Without the replace attribute, the default checkout behavior is to remove the value from the mel entity. This would cause all the waiting Inc tasks to wait forever since there is no new writer task to deposit a fresh value to the mel entity to satisfy the reader zone entry condition.

Back to the main task, we see that it wants to display the final value of SafeCount[somekey]. Since it does not know when all the pelled Inc tasks will all finish, it sleeps for 1 empirical second. To access the mel value at that time, the main task just use the mel read statement <?>SafeCounter[somekey]. There is no need for using a mel read exclusive zone since a NERWous C mel read natively enforces mutual exclusion.

Version 2:
Let's revise the above implementation. Since NERWous C supports inline code blocks, we can drop the Inc function and move its work inside the main task:
/* VERSION 2 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i) {
      <!> <?>SafeCounter[somekey] {
      SafeCounter[somekey]++;
      <checkout replace>;
      }
   }
   sleep (1000);
   printf ("%d", <?>SafeCounter[somekey]);
}
Version 3:
Since auto-increment (and decrement) are frequently used operations, NERWous C supports shortcuts that skip mentioning the explicit use of the mel read zone:
/* VERSION 3 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i)
      <!> <?>SafeCounter[somekey]++;

   sleep (1000);
   printf ("%d", <?>SafeCounter[somekey]);
}
Version 2 and 3 are the same. In both Versions, the mel entity is replaced in place, thus preserving its sequence number. The next task that accesses the mel entity will see a new value but the same sequence number. Sequence numbers are used to prevent re-processing of stale values. This is not an issue with the Mutex example here, since each pelled task that does the auto-increment goes through the SafeCounter[somekey] mel entity once and does not do loops. Loops are where stale values can be encountered.

Version 4:
In the previous versions, we have a sleep operation because we want to display the final value of SafeCounter[somekey], but since the auto-increment tasks run in parallel, we don't know when they all finish. The one second is an empirical value that seems to work in the running environment. (If it does not, just increment it until it does.) NERWous C supports a pel-for loop that allows us to remove the sleep operation:
/* VERSION 4 */
#include "keys.h"
<mel>int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      <?>SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   <begin> { for (int i=0; i<1000; ++i)
      <!> <?>SafeCounter[somekey]++;
   } <? ENDED>;

   printf ("%d", <?>SafeCounter[somekey]);
}
Encapsulating the for loop inside the begin-wait-ended block ensures that all pelled tasks in the for loop have ended before the programming flow continues. When main issues the printf command in Version 4, it is assured that all auto-increment tasks are done and the mel entity SafeCounter[somekey] contains the final value.

Version 5 (Serial):
There is no NERWous C Version 5. Version 4 is as compact as it can be. However notice that if we strip out the NERWous C pel and mel constructs, we end up with the correspondent C program:
/* VERSION 5(Serial)  */
#include "keys.h"
int SafeCounter[NUMKEYS];

void main () {
   for (int i=0; i<NUMKEYS; ++i)
      SafeCounter[i] = 0;

   int somekey = random(0, NUMKEYS);
   for (int i=0; i<1000; ++i)
      SafeCounter[somekey]++;

   printf ("%d", SafeCounter[somekey]);
}
Sometimes, with NERWous C, by sprinkling some <?> and <!> here and there, we transform a serial program into a concurrent program without much ado.


Previous Next Top

Friday, December 29, 2017

Compound Reader Zones

Welcome » NERWous C » Mel
  1. Reader OR Zones
  2. Reader LIST Zones
  3. Reader AND Zones
In the previous chapter, we have been introduced to exclusive reader zones as applicable to a single mel variable. In this chapter we expand the discussion of reader zones to multiple mel variables. A future chapter will cover reader zones used with structured mels.


Reader OR Zones

We start with the reader OR zone. Let's look at this example where we have two mel variables worked on by three tasks: a Producer, a Consumer, and a Manipulator that Manipulates all the things that are Produced before being Consumed:
#include "nerw.h"
main () {
   <mel> int store1, store2;
   <pel> p = <!> Manipulator (store1, store2);
   <!> Producer (store1, store2);
   <!> Consumer (store1, store2);
}
void Producer (<mel> int store1, <mel> int store2) {
   while ( <?>(store1 || store2) = Produce() );
   <close>store1;
   <close>store2;
}
void Consumer (<mel> int store1, <mel> int store2) {
   while ( true ) {
     try Consume(<?>(store1 || store2));
     catch((store1 && store2)<CLOSED>) break;
   }
}
void Manipulator (<mel> int store1, <mel> int store2) {
   try <? priority=NERW_PRIORITY_HIGHEST as=store> (store1 || store2) {
      store = Manipulate(store);
      <checkout writeover>;
   } <resume>;
   catch ( (store1 && store2)<CLOSED> ) {}
   printf ("Manipulator is done");
}
The two mel channels back up one another. If Producer finds store1 not available, it will try to deposit its product to store2. When Produce generates a zero product, Producer will break out of the production loop and closes both mel channels. Likewise, the Consumer continuously tries to Consume from either store1 or store2 whichever is available, until it gets a CLOSED exception on both channels. Both tasks use the mel OR read wait facility.

Reader OR Zone Access

As said previously, the goal of the Manipulator task is to capture all the items from Producer via either the store1 or store2 channel, do some manipulation on these items before releasing them to the Consumer task. This is an iteration of the Manipulator for single mel variable we have seen in the previous chapter. To realize this goal for two mel variables, the Manipulator here also makes use of an exclusive zone to do the manipulation, but depends on the mel OR wait on the mel channels.

This is the behind-the-scene process:
  1. The Manipulator task puts itself to store1 readers' queue. In the example above, it goes into the NERW_PRIORITY_HIGHEST priority readers' queue so that it can get to any produced items before Consumer.
     
  2. If it happens (1) to be first on the queue and (2) store1 is valued and (3) this value is not stale, then it will check into the exclusive reader zone, and invokes Manipulate on the eponymous variable store representing store1.
     
  3. If one of the conditions for store1 is false, Manipulator will put itself in the NERW_PRIORITY_HIGHEST priority readers' queue of store2. If it happens (1) to be first on this queue and (2) store2 is valued and (3) this value is not stale, then it will check into store2 exclusive reader zone, and invokes Manipulate on the eponymous variable store representing store2.
     
  4. Otherwise, Manipulator will wait on both queues at the same time.
     
  5. On the first queue that satisfies all 3 conditions (task first on the queue, mel is valued, and the value is not stale), Manipulator will check into the reader zone with that mel variable. Since this can be either store1 or store2, Manipulator uses the generic store eponymous variable.
     
  6. Inside the reader zone, the task makes reads and writes to the eponymous variable store. Since this is a local variable, the mel wait operator (<?>) is not applicable.
     
  7. After manipulation, the Manipulator task invokes the <checkout writeover> operation to update the mel channel and get out of the reader zone. The task remembers what mel channel is used on check-in so that the checkout behavior will be applied to that mel channel.
     
  8. On checkout of the reader zone, the Manipulator task gets off both priority wait queues - the one for the mel channel it has checked in, and the one for the mel channel it still waiting on. However, as there is a <resume> operator used in our example, the task is put back on both waiting queues right away.

Reader OR Zone Traverse

The behind-the-scene description above uncovers a subtle difference between
<? priority=NERW_PRIORITY_HIGHEST as=store> (store1 || store2)
and
<? priority=NERW_PRIORITY_HIGHEST as=store> (store2 || store1)
The first mel item in the OR list is checked first. If it is re-valued constantly by a writer task, the mel wait on that first item is likely successful and the reader 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 reads:
<? priority=NERW_PRIORITY_HIGHEST as=store> (store1 ||<random> store2)
With the random traverse behavior, the check for store1 and store2 is randomized instead of serialized.

Reader OR Zone Resumption

On checkout of the reader zone, the Manipulator invokes the <resume> operation to jump back to the mel OR zone entrance, waiting for either store1 or store2 again. The task is put at the back of the queues, but since it is the only task for the NERW_PRIORITY_HIGHEST queue, it is at the top of both queues again.

Reader OR Zone Exception

If one channel has been closed, the mel OR wait will focus solely on the remaining channel. If both channels have been closed, the (store1 && store2)<CLOSED> exception will be raised, causing the Manipulator to abort the waiting for the exclusive zone. In the above example, the Manipulator displays the printf statement before it ends.

Reader OR Zone Cases

The previous Manipulator does not care if it selects store1 or store2. It processes using the stand-in store the same. What if it does make a case of doing store1 somewhat differently from store2? Let's explore such a case:
void Manipulator (<mel> int store1, <mel> int store2) {
   try <? priority=NERW_PRIORITY_HIGHEST as=store> (store1 || store2) {
      printf ("Select [%ll] to manipulate", store<id>);
      if ( store<id> == store1<id> )
         store = Manipulate_1 (store);
      else
         store = Manipulate_2 (store);
      <checkout writeover>;
   } <resume>;
   catch ( (store1 && store2)<CLOSED> ) {}
   printf ("Manipulator is done");
}
By using the <id> property, Manipulator can make a case of using either Manipulate_1 or Manipulate_2 depending on what mel variable is selected.

Reader OR Zone Gotcha!

A knowledgeable reader will see that the above implementation of Manipulator is not correct. It will let Producer items slipped by and gone directly to the Consumer without being Manipulated. For example, while Manipulator is working on store1, store2 is available for Producer to deposit a new product and Consumer to consume it.

The correct solution is not to use the reader OR zone, but to use two single reader zones, one for store1 and the other for store2:
main () {
   <mel> int store1, store2;
   <pel> p1 = <!> Manipulator (store1);
   <pel> p2 = <!> Manipulator (store2);
   <!> Producer (store1, store2);
   <!> Consumer (store1, store2);
}
void Producer (<mel> int store1, <mel> int store2) {
   while ( <?>(store1 || store2) = Produce() );
   <close>store1;
   <close>store2;
}
void Consumer (<mel> int store1, <mel> int store2) {
   while ( true ) {
      try Consume(<?>(store1 || store2)); 
      catch((store1 && store2)<CLOSED>) break;
   }
}
void Manipulator (<mel> int store) {
   try <? priority=NERW_PRIORITY_HIGHEST>(store) {
      store = Manipulate(store);
      <checkout writeover>;
   } <resume>;
   catch ( store<CLOSED> ) {}
   printf ("Manipulator for [%s] is done", store<name>);
}
Sometimes it is necessary to use a bad example to introduce a new feature.


Reader LIST Zones

In the previous examples, we have Producer and Consumer take in two mel variables but use only one of them. Let's modify the example so that these tasks make use of both of them. This allows us to also change the Manipulator to make use of the reader LIST zone.
main () {
   <mel> int store1, store2;
   <pel> p = <!> Manipulator (store1, store2);
   <!> Producer (store1, store2);
   <!> Consumer (store1, store2);
}
void Producer (<mel> int store1, <mel> int store2) {
   while ( <?>(store1, store2) = ProduceTwoItems() );
   <close>(store1 && store2);
}
void Consumer (<mel> int store1, <mel> int store2) {
   while ( true ) {
     try ConsumeTwoItems(<?>(store1, store2));
     catch((store1 || store2)<CLOSED>) break;
   }
}
void Manipulator(<mel> int store1, <mel> int store2) {
   try <? priority=NERW_PRIORITY_HIGHEST>(store1, store2) {
      store1 = Manipulate(store1);
      store2 = Manipulate(store2);
      <checkout writeover>;
   } <resume>;
   catch ( (store1 || store2)<CLOSED> ) {}
}
This is the behind-the-scene process for a reader LIST zone access:
  1. The Manipulator task puts itself to both store1 and store2 NERW_PRIORITY_HIGHEST priority queues.
     
  2. In each queue whenever (1) the Manipulator task becomes first on the queue and (2) the corresponding mel is valued and (3) this value is not stale, then it will get hold to that mel and waits for the other queue to also satisfy those three conditions.
     
  3. While waiting on a queue at the top of the queue position, the Manipulator task allows another reader task to get a stale value. However it will not allow another reader task to get a new value.
     
  4. Once it can get hold of both the required mels, the Manipulator task will check into the exclusive reader zone.
     
  5. From this time on, no reader task can get the blocked mel values. Non-intrusive readonly access and snapshot operation are still permissible.
     
  6. In the reader zone, the Manipulator task uses the local eponymous variables, which are initialized with the original values of the remote mel variables.
     
  7. Once it is done with the manipulations, the Manipulator task invokes the <checkout writeover> to replace the values of the mel variables with the values of the corresponding eponymous variables.
     
  8. On checkout of the reader zone, the Manipulator task gets off both priority wait queues. However, since there is a <resume> operator, the task is put back on both waiting queues right away.

Since the reader LIST zone requires both mels, a closure of either one is bad for Manipulator. This is the reason its catch on the CLOSED exception uses an OR clause. Compare this with the reader OR zone example where the Manipulator triggers on an AND CLOSED exception.

Reader LIST Zone Gotcha!

Can a produced item sneaked by from the Producer to the Consumer without going through the Manipulator? This can happen with the reader OR zone, but not with the reader LIST zone.

Like the reader OR zone Manipulator, the reader LIST zone Manipulator is always present in the higher priority queue than the Consumer, thus it always has first dip to the mel variables. Unlike the reader OR zone version though, the reader LIST zone Manipulator blocks both mel variables when it is in the reader zone, preventing the Consumer to sneak by.

On the other hand, a Manipulator-like task can introduce starvation. If instead of checking out with a <checkout writeover> which leaves the mel value available for Consumer, it were to use <checkout> which would remove the mel value, the Consumer would never see a product from Producer.


Reader AND Zones

The reader AND zone uses the mel AND wait in order to have exclusive access to all the specified mel items.

Let's rewrite the Manipulator task using a reader AND zone:
void Manipulator(<mel> int store1, <mel> int store2) {
   try <? priority=NERW_PRIORITY_HIGHEST>(store1 && store2) {
      store1 = Manipulate(store1);
      store2 = Manipulate(store2);
      <checkout writeover>;
   } <resume>;
   catch ( (store1 || store2)<CLOSED> ) {}
}
The above Manipulator is a bad Manipulator because it will allow products from the Producer to slip by and go directly to the Consumer. Let's see how so.
  1. The Manipulator task puts itself to both store1 and store2 NERW_PRIORITY_HIGHEST priority queues.
     
  2. In each queue whenever the Manipulator task becomes first on the queue, it will join the "top-of-the-queue" readers group, as specified by the mel AND wait process. It then waits to join the "top-of-the-queue" readers group of the other requested mel variable.
     
  3. When the Manipulator task is in a "top-of-the-queue" group for one mel reader's queue but not the other, it will allow other reader tasks to "jump the line" on that queue to get the mel value -- stale or new, in accordance with the mel AND wait process.
     
  4. When the Manipulator task is in the "top-of-the-queue" groups of both mel variables, it will check if both mel variables are (1) valued and (2) not stale. If one of the conditions is not true for either mel, the task keeps waiting. During this wait, it will allow other reader tasks to "jump the line" on both queues to get the mel values -- stale or new.
     
  5. Once all the conditions are met at both queues, the Manipulator task blocks both mels at the same time, and checks into the exclusive reader zone.
     
  6. From this time on, no reader task can "jump the line" and get the mel values. Non-intrusive readonly access and snapshot operation are still permissible.
     
  7. In the reader zone, the Manipulator task uses the local eponymous variables, which are initialized with the values of the mel variables.
     
  8. Once it is done with the manipulations, the Manipulator task invokes the <checkout writeover> to replace the values of the mel variables with the values of the corresponding eponymous variables.
     
  9. On checkout of the reader zone, the Manipulator task gets off both priority wait queues. However, since there is a <resume> operator, the task is put back on both waiting queues right away.

The use of the "top-of-the-queue" groups allows other reader tasks to "jump the line" and get the mel value even if this mel value is new to the Manipulator task. In our example, this reader task is the Consumer task and "jumping the line" will allow it to consume a product raw without being Manipulated.

On the other hand, the use of "top-of-the-queue" groups prevents deadlocks when multiple tasks vie for the same resources in a circular way, as in the The Dining Philosophers example. Unlike the readers LIST zone where the tasks get hold to the mel variables by themselves without knowledge of similar needs of other tasks, the readers AND zone method collects all such needs in "top-of-the-queue" groups where the NERW runtime has full knowledge to prevent deadlocks when granting exclusive access.


Previous Next Top

Tuesday, December 12, 2017

CommonJS Promises/A Example

Welcome » NERWous C » Examples
  1. CommonJS Promises
  2. NERWous C Sample


CommonJS Promises

The goal of the CommonJS group is to build a better JavaScript ecosystem. One of its proposal is Promises/A: "a promise represents the eventual value returned from the single completion of an operation".

The following example to display the web contents of the first web link in a tweet, is taken from an article that exalts the use of Promises/A:
getTweetsFor("domenic") // promise-returning function
  .then(function (tweets) {
    var shortUrls = parseTweetsForUrls(tweets);
    var mostRecentShortUrl = shortUrls[0];
    return expandUrlUsingTwitterApi(mostRecentShortUrl); // promise-returning function
  })
  .then(httpGet) // promise-returning function
  .then(
    function (responseBody) {
      console.log("Most recent link text:", responseBody);
    },
    function (error) {
      console.error("Error with the twitterverse:", error);
    }
  );
The promise of Promises/A is that promises can be chained, and exceptions can bubble up to someone who can handle that failure.

NERWous C Sample

The verbose first version shows all the tasks:
/* VERSION 1 - Asynchronous */
try {
   <pel> p_tweets = <!> getTweetsFor("domenic");    // parallel execution
   char* tweets = <?>p_tweets;     // the mel wait unblocks the thread for other tasks
   char* shortUrls = parseTweetsForUrls(tweets);     // serial execution
   char* mostRecentShortUrl = shortUrls[0];          // serial execution
   <pel> p_twitter = <!> expandUrlUsingTwitterApi(mostRecentShortUrl);    // parallel
   char* url = <?>p_twitter;     // mel wait unblock the thread for other tasks
   <pel> p_http = <!> httpGet(url);     // parallel execution
   char* responseBody = <?>p_http;     // mel wait unblock the thread for other tasks
   printf ("Most recent link text: %s", responseBody);
}
catch (p_tweets<...>) {
   printf ("Error [%s] on [getTweetsFor] due to [%s]",
      p_tweets<exception>, p_tweets<why>);
}
catch (p_twitter<...>) {
   printf ("Error [%s] on [expandUrlUsingTwitterApi] due to [%s]",
      p_twitter<exception>, p_twitter<why>);
}
catch (p_http<...>) {
   printf ("Error [%s] on [httpGet] due to [%s]",
      http<exception>, http<why>);
}
The 2nd version is more compact, and shows that the tasks can be chained together, and that errors can bubble to the top exception handler:
/* VERSION 2 - Asynchronous */
try {
   char* shortUrls = parseTweetsForUrls(<!><?> getTweetsFor("domenic"));
   char* mostRecentShortUrl = shortUrls[0];
   char* responseBody = <?><!> httpGet(
      <?><!> expandUrlUsingTwitterApi(mostRecentShortUrl)
      );
   printf ("Most recent link text: %s", responseBody);
}
catch (pel<...>) {
   printf ("Error [%s] on [%s] due to [%s]",
      pel<exception>, pel<name>, pel<why>);
}
The double header <?><!> means forking the task to run in parallel (<!>) and then waiting for its result (<?>).

The generic keyword pel in the catch statement represents a task that fails. The name of the task is found via the property pel<name>.

Removing the NERWous C symbols from the compact version above results in the synchronous C version:
/* VERSION 3 - Synchronous */
try {
  char* shortUrls = parseTweetsForUrls(getTweetsFor("domenic"));  /* blocking */
  char* mostRecentShortUrl = shortUrls[0];
  char* responseBody = httpGet(
     expandUrlUsingTwitterApi(mostRecentShortUrl)
     ); // blocking x 2
   printf ("Most recent link text: %s", responseBody);
} catch (error) {
  printf("Error with the twitterverse: %s ", error);
}
In other words, it is sometimes very easy to transform a serial synchronous version into a parallel asynchronous version using NERWous C. Just pepper it juicily with pel (<!>) and mel (<?>) constructs.


Previous Next Top

Wednesday, December 6, 2017

Concurrent Programming In Scala

Welcome » NERWous C » Examples
  1. Scala Language
  2. Actors Model
  3. Parallel Collections
  4. Futures and Promises


Scala Language

Publicly released in 2004, the Scala programming language was originally designed to be more concise than the Java language and with functional programming features missing in Java at that time. Since then the Scala language has been expanded from running solely on Java Virtual Machine to run on other platforms, such as Javascript. Current information about the language can be found on its official web site, www.scala-lang.org.

Scala supports parallel and concurrent programming via the following features:
  1. Actors Model
  2. Parallel Collections
  3. Futures and Promises
For each feature, let's study an example written in Scala, and see how it can be rewritten similarly in NERWous C.


Actors Model

Scala Actors are concurrent processes that communicate by exchanging messages.

Scala Actors - Ping-Pong Example

This ping-pong example uses the deprecated Scala Actors library. Scala Actors has now migrated to Akka. However since the ping-pong example is described in better details with the Scala Actors article than with the Akka terse documentation, it is used here.
case object Ping
case object Pong
case object Stop
import scala.actors.Actor
import scala.actors.Actor._
class Ping(count: int, pong: Actor) extends Actor {
  def act() {
    var pingsLeft = count - 1
    pong ! Ping
    while (true) {
      receive {
        case Pong =>
          if (pingsLeft % 1000 == 0)
            Console.println("Ping: pong")
          if (pingsLeft > 0) {
            pong ! Ping
            pingsLeft -= 1
          } else {
            Console.println("Ping: stop")
            pong ! Stop
            exit()
          }
      }
    }
  }
}
class Pong extends Actor {
  def act() {
    var pongCount = 0
    while (true) {
      receive {
        case Ping =>
          if (pongCount % 1000 == 0)
            Console.println("Pong: ping "+pongCount)
          sender ! Pong
          pongCount = pongCount + 1
        case Stop =>
          Console.println("Pong: stop")
          exit()
      }
    }
  }
}
object pingpong extends Application {
  val pong = new Pong
  val ping = new Ping(100000, pong)
  ping.start
  pong.start
}
The ping-pong example has a Ping actor sending to the Pong actor a "Ping" message. Upon receiving it, the Pong actor replies with a "Pong" message. After 1000 interactions, the Ping actor is done playing, and sends a "Stop" message. When Pong receives the "Stop", it also stops playing.

NERWOUS C Version 1

The first version rewrite in NERWous C hews to the Scala example, by using a receive input mel argument to represent the message sending between Scala actors.
main () {
   <pel>pong = <! name="Pong">Pong (null);
   <pel>ping = <! name="Ping">Ping (100000, null);
}
void Ping (int count, <mel> string receive) {
   <pel>pong;
   <? pel name="Pong" started>pong;

   int pingsLeft = count - 1;
   <?>pong.receive = "Ping";
   while ( 1 ) {
      switch ( <?>receive ) {   /* wait to receive */
         case "Pong";
            if (pingsLeft % 1000 == 0)
               printf ("Ping: pong");
            if (pingsLeft > 0) {
               <?>pong.receive = "Ping";
               --pingsLeft;
            } else {
               printf ("Ping: stop");
               <?>pong.receive = "Stop";
               <return>;
            }
            break;
         }
      }
   }
}
void Pong (<mel> string receive) {
   <pel>ping;
   <? pel name="Ping" started>ping;

   int pongCount = 0;
   while ( 1 ) {
      switch ( <?>receive ) {
         case "Ping":
            if (pongCount % 1000 == 0)
               printf ("Pong: ping " + pongCount);
             <?>ping.receive = "Pong";
             ++pongCount;
             break;
        case "Stop":
           printf ("Pong: stop");
           <return>;
      }
   }
}
The tricky thing about the Ping-Pong example is how Pong knows about Ping since it is created before Ping ever exists. The Scala solution is to use the sender actor which represents the actor that sends the message. When Pong receives the "Ping" message, the sender is the de facto Ping actor. The NERWous C version uses the wait-for-pel statement, which allows the task Pong to wait for a task named "Ping" to have a certain state (here, we pick the started state), and initializes the local pel variable ping with information about the task named "Ping":
<? pel name="Ping" started>ping;
Scala uses the receive method for an actor to send messages to another actor. The NERWous C version above uses the mel input argument. It is named receive here but can be any valid name. When Ping first runs, it sends a "Ping" message to Pong's receive mel input argument:
<?>pong.receive = "Ping";
In the mean time, the Pong task waits on its receive mel input argument to be valued with a message, either "Ping" or "Stop". With the former, it sends back a "Pong" message. With the latter, it just quits by running the <return> statement.
A computer linguist will notice that the C language on which NERWous C is based, does not support the native type string. It is used here for code simplicity since the focus is on the concurrency features, and not on the base language.

NERWOUS C Version 2

Let's now rewrite Version 1 to use NERWous C "streaming" feature. Instead of having Ping sending a "Ping" message directly to Pong, we will have Ping stream its "Ping" messages via the release operation to its mel output argument, and Pong access Ping's output messages:
main () {
   <pel>pong = <! name="Pong">Pong ();
   <pel>ping = <! name="Ping">Ping (100000);
}
<mel> string Ping (int count) {
   <pel>pong;
   <? pel name="Pong" started>pong;

   int pingsLeft = count - 1;
   <release> "Ping";   /* stream first "Ping" */

   <?>pong;   /* wait for Pong to stream */
   if (pingsLeft % 1000 == 0)
      printf ("Ping: pong");
   if (pingsLeft > 0) {
      <release> "Ping";   /* stream "Ping" again */
      --pingsLeft;
      <resume>;    /* resume the wait for Pong to stream */
   } else {
      printf ("Ping: stop");
   }
}
<mel> string Pong () {
   <pel>ping;
   <? pel name="Ping" started>ping;

   int pongCount = 0;
   try {
      <?>ping;   /* wait for Ping to stream */
      if (pongCount % 1000 == 0)
         printf ("Pong: ping " + pongCount);
      <release> "Pong";   /* stream "Pong" */
      ++pongCount;
      <resume>;    /* resume the wait for Ping to stream */
   }
   catch ( ping<ENDED> ) { }
}
Two changes are being made in Version 2. The first one is that the while loop has been replaced by the resume operation that repeats the mel wait for the streaming messages. The second one is that the "Stop" message has been removed. The Pong task knows that Ping has ended via the ENDED exception.


Parallel Collections

The parallel collections feature in Scala is discussed in this article. The examples to illustrate this feature are:
  1. Map
  2. Fold
  3. Filter
Map

This example uses a parallel map to transform a collection of String to all-uppercase:
val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par
lastNames.map(_.toUpperCase)
The result of the run is:
SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN

The NERWous C version is more verbose since there is no built-in map function. Again, for simplicity, we we will use the fictitious string type which does not exist in the C language:
/* VERSION 1 */
#define NUM 6
string lastNames[NUM] = {"Smith","Jones","Frankenstein","Bach","Jackson","Rodin"};
for (int i=0; i<NUM; ++i) {
   lastNames[i] = <!> toUpperCase(lastNames[i]);
}
The NERWous C version has a serial loop that pels a new inline task in each iteration. The inline task takes in lastNames[i] as a local variable, and ends with its toUpperCase value. The returned value is then re-assigned to the local variable lastNames[i].

Due to the assignment back to lastNames[i] which is under the main task context, each inline task has to finish running before the next inline task can run. So although the inline tasks can run in parallel with one another, they are practically run serially, albeit on a possibly different cel element.

Let's now transform lastNames into a mel array so that it can be accessed in parallel by the toUpperCase inline tasks:
/* VERSION 2 */
#define NUM 6
<mel>string lastNames[NUM] = {"Smith","Jones","Frankenstein","Bach","Jackson","Rodin"};
for (int i=0; i<NUM; ++i) {
   <!> { <? replace>lastNames[i] = toUpperCase(?); }
}
Each inline task that is pelled on each iteration of the for loop now truly runs in parallel, each accessing its portion of the shared mel array lastNames. Each inline task uses the reader zone shortcut to replace in place the value of the mel element lastNames[i].

Fold

This example adds up the values from 1 to 10000.
val parArray = (1 to 10000).toArray.par
parArray.fold(0)(_ + _)
The result of 1 + 2 + 3 + ... + 10000 is 50005000. Again, the NERWous C version is much more verbose since there is no built-in fold capability:
#define NUMCOUNT 10000
int parArray[NUMCOUNT];
for (i = 0; i<NUMCOUNT; ++i)
   parArray[i] = i+1;    /* initialize array */

<mel> int sum;
<collect> for (i = 0; i<NUMCOUNT; i += 2 ) {
    <! import="parArray[i] ..<flex ubound=NUMCOUNT uvalue=0>.. parArray[i+1]"> {
        <?> sum += (parArray[i] + parArray[i+1]);
    }
} <? ENDED>;
printf ("Summation value %d", <?>sum);
The first for loop initializes the parArray array with values 1, 2, etc. to 10000. The second for loop takes every two elements of the array and do the summation in separate tasks. Each task gets their local parArray elements imported, does the addition, and adds up the result into the share sum mel variable.

The second for loop is run inside a collect-ENDED block to make sure that all tasks that it pels have ended before the program continues with the printf statement. This ensures that the mel variable sum contains all the summations.

Each iteration in the second for loop creates a separate inline task to do the summation. This task requires two local values passed via the import operation. The import statement could be written simply as:
<! import="parArray[i],parArray[i+1]">
However if the number of items in parArray is odd, the last iteration of the for loop will generate an out-of-bound exception on parArray[i+1]. The use of the flex consecontive construct addendum allows the run-time environment to assign the uvalue value (0) to any parArray item with an index equal to or greater than the ubound value (NUMCOUNT).

Filter

This example uses a parallel filter to select the last names that come alphabetically after the letter “J”.
val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par
lastNames.filter(_.head >= 'J')
The result of the run is Smith, Jones, Jackson, Rodin. This is the NERWous C version:
#define NUM 6
string lastNames[NUM] = { "Smith","Jones","Frankenstein","Bach","Jackson","Rodin" };
for (int i=0; i<NUM; ++i) {
   <!> { if head(lastNames[i]) >= 'J') printf("%s", lastNames[i]); }
}
Each iteration of the for loop pels a new task that runs independently from each other. Each task gets its local array element lastNames[i] imported for processing. Since each task runs independently and concurrently, the order of the names printed out is not deterministic.


Futures and Promises

Under Scala, a future is a read-only reference to a yet-to-be-completed value, while a promise is used to complete a future either sucessfully or with an exception and failure.

Futures - Coffee Making Example

The coffee making example is described in this article. The complete code is taken from GitHub:
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.util.Random
import scala.util.Try

object kitchen extends App {

  /////////////////////////////
  // Some type aliases, just for getting more meaningful method signatures:
  type CoffeeBeans = String
  type GroundCoffee = String
  case class Water(temperature: Int)
  type Milk = String
  type FrothedMilk = String
  type Espresso = String
  type Cappuccino = String

  // some exceptions for things that might go wrong in the individual steps
  // (we'll need some of them later, use the others when experimenting
  // with the code):
  case class GrindingException(msg: String) extends Exception(msg)
  case class FrothingException(msg: String) extends Exception(msg)
  case class WaterBoilingException(msg: String) extends Exception(msg)
  case class BrewingException(msg: String) extends Exception(msg)
  /////////////////////////////
  def combine(espresso: Espresso, frothedMilk: FrothedMilk): Cappuccino = "cappuccino"

  def grind(beans: CoffeeBeans): Future[GroundCoffee] = Future {
    println("start grinding...")
    Thread.sleep(Random.nextInt(2000))
    if (beans == "baked beans") throw GrindingException("are you joking?")
    println("finished grinding...")
    s"ground coffee of $beans"
  }

  def heatWater(water: Water): Future[Water] = Future {
    println("heating the water now")
    Thread.sleep(Random.nextInt(2000))
    println("hot, it's hot!")
    water.copy(temperature = 85)
  }

  def frothMilk(milk: Milk): Future[FrothedMilk] = Future {
    println("milk frothing system engaged!")
    Thread.sleep(Random.nextInt(2000))
    println("shutting down milk frothing system")
    s"frothed $milk"
  }

  def brew(coffee: GroundCoffee, heatedWater: Water): Future[Espresso] = Future {
    println("happy brewing :)")
    Thread.sleep(Random.nextInt(2000))
    println("it's brewed!")
    "espresso"
  }

  ///////////// Business logic
  println("Kitched starting")
  def prepareCappuccino(): Future[Cappuccino] = {
    val groundCoffee = grind("arabica beans")
    val heatedWater = heatWater(Water(20))
    val frothedMilk = frothMilk("milk")
    for {
      ground <- groundCoffee
      water <- heatedWater
      foam <- frothedMilk
      espresso <- brew(ground, water)
    } yield combine(espresso, foam)
  }
  val capo = prepareCappuccino()
  Await.ready(capo, 1 minutes)
  println("Kitched ending")
}
The NERWous C version has the main task pels the tasks to do the grinding (grind), heating water (heatWater), frothing the milk (frothMilk) and brewing the espresso (brew) -- to be run in parallel. The brew task waits for the grind task to have the coffee beans ground, and the heatWater task to have the water hot to the requested temperature. The main task, after pelling all the activities, waits for the brew and frothMilk to be done before declaring "Kitchen ending". If the brewing is not ready within a minute, the main task ends with "Kitchen ending without espresso".
main () {
   printf ("Kitchen starting");
   <pel> ground = <!> grind("arabica beans");
   <pel> water = <!> heatWater(Water(20));
   <pel> foam = <!> frothMilk("milk");
   <pel> espresso = <!> brew(ground, water);

   try {
      string frothMilk_ret, brew_ret;
      (frothMilk_ret, brew_ret) = <? timeout=6000> (foam, espresso);
      if ( brew_ret == "" ) printf ("Kitchen ending without espresso");
      else printf ("Kitchen ending");
   }
   catch ((foam && espresso)<TIMEOUT>) {
      printf ("Espresso and milk not ready within 1 min");
   }
}

string grind( string beans ) {
    printf("start grinding...");
    sleep(rand()%2000);
    if ( beans == "baked beans") return "are you joking?";
    printf("finished grinding...");
    return("ground coffee of " + beans);
 }

 int heatWater(int temp) {
    printf("heating the water now")
    sleep(rand()%2000);
    printf("hot, it's hot!")
    return 85;
 }

 string frothMilk( string milk ) {
    printf("milk frothing system engaged!")
    sleep(rand()%2000);
    printf("shutting down milk frothing system");
    return "frothed " + milk;
 }

 string brew( pel ground, pel water ) {
    printf("happy brewing :)");
    sleep(rand()%2000);

    string ret = <?> ground;
    if ( ret == "are you joking?" ) return "";

    <?> water;
    printf("it's brewed!");
    return "espresso";
 }
The tasks communicate their ending via the return statement with a string value. The tasks that depend on those ending tasks, start to do their own stuff, then wait for those returned values before continuing with the rest of their own stuff.

Promises - Producer/Consumer Example

This producer/consumer example is taken from the Scala SIP-14 document.
import scala.concurrent.{ Future, Promise }
val p = Promise[T]()
val f = p.future

val producer = Future {
  val r = someComputation
  if (isInvalid(r))
    p failure (new IllegalStateException)
  else {
    val q = doSomeMoreComputation(r)
    p success q
  }
}

val consumer = Future {
  startDoingSomething()
  f onSuccess {
    case r => doSomethingWithResult(r)
  }
}
The NERWous C version has the main task pelling the producer and consumer tasks to run in parallel.
main () {
   <pel> prod = <!> producer();
   <!> consumer(prod);
}
int producer () {
   int r = someComputation();
   if (isInvalid(r) ) <end FAILED>;
   else {
      int q = doSomeMoreComputation(r);
      <return> q;
   }
}
void consumer (<pel>prod) {
   startDoingSomething();
   try {
      doSomethingWithResult (<?>prod);
   }
   catch (prod<FAILED>) {}
   catch (prod<...>) {}
}
The producer task either ends with a FAILED exception on an invalid someComputation result, or ends normally with a valid result to be used as the return value of the task.

The consumer task is given a representative of the producer task via the pel input argument prod. It does startDoingSomething then waits for producer return value via the mel wait statement <?>prod. If the wait is successful, it will do doSomethingWithResult. If the wait fails due to a FAILED or other exception, the consumer just ends without doing anything more.


Previous Next Top