ANU crest

Research School of
Computer Science

Concurrent & Distributed
Systems
(comp2310, comp6310)

Lectures
<empty space> Schedule
<empty space> Contents
(incl. slides)
<empty space>
Laboratories
<empty space> Setup & manuals
<empty space> Weekly labs
<empty space> Assignments
<empty space>
Assessment
<empty space> Examination
<empty space>
Resources
<empty space> References
<empty space> Sources
<empty space> Description
<empty space> Forum

<empty space>

Laboratory 5

Rendezvous, Requeue and Task Entry Families


  • If you did not get to complete "Entry calls and barriers" from L3, go back and look at this section.
  • The labs are designed to make you think. It is not expected that you will complete them in 2 hours. Rather I would expect most of you to take around 4.5 hours - 0.5 hours to read it before your lab, 2 hours in your assigned lab, and 2 hours afterwards. If you have questions concerning a previous lab, ask your tutor in this lab, or come and talk to me (Uwe). Also, if you want to drop in on any other lab session, that is possible, providing room is available.
  • The gnat environment is only available on the linux systems. To work from home you must ssh to machine partch.


Rendezvous Part 2


In laboratory 3 you were introduced to the Ada rendezvous. In this communication primitive one task - the server - posts an accept call while the another task - the client - makes the connection. It is important to note that the communication is asymmetric, in that the server task will accept any incoming connection of that name regardless of which client task makes the connection.

The above is restrictive in that once either the server or client task has initiated their request for a rendezvous both tasks are blocked from executing any further instructions until that rendezvous has been made. The select statement, and use of conditional and/or timed entry calls allow this constraint to be removed.

The Select Statement

The select statement enables the server task to:
  • wait for more than a single rendezvous at any one time;
  • time-out if no rendezvous is forthcoming within a specified period;
  • withdraw its offer to communicate if no rendezvous is immediately available;
  • terminate if no clients can possibly call its entries.
The syntax of the select statement is:
    selective_accept ::=
       select
          [guard]
          selective_accept_alternative
    {  or
          [guard]
          selective_accept_alternative}
    [  else
          sequence_of_statement]
       end select;

    guard ::= when =>

    selective_accept_alternative ::= accept_alternative | delay_alternative | terminate_alternative

    accept_alternative ::= accept_statement [sequence_of_statements]

    delay_alternative ::= delay_statement [sequence_of_statements]

    terminate_alternative ::= terminate;

Guarded Alternatives

The guard is a boolean expression which is evaluated when the select statement is executed. If the expression evaluates to true then the alternative is eligible for selection. If it evaluates to false the alternative is not eligible for selection.

Note that the boolean expression is only evaluated once per execution of the select statement. Also, it is considered an error in the logic of the program if a selective accept statement has a guard on each of its alternatives and all the guards evaluate to false. When this happens the exception Program_Error is raised.

Delay Alternative

The selective accept form of the select statement allows a server task to time-out if an entry call is not received within a certain period of time. The time-out is expressed using the delay statement and can be either a relative of an absolute delay. If the relative time expressed is zero or negative, or the absolute time has passed, the delay alternative is equivalent to having an "else part" in the select statement (see below). Delay statements may have guards, in which case if the guard evaluates to false on execution of the selection statement it is not considered for selection.

Else Alternative

As well as allowing a server task to time-out in the absence of a call, the selective accept form of the select statement also allows the server to withdraw its offer to communicate if no call is immediately available.

Terminate Alternative

A server task which is suspended at a select statement with an open terminate alternative will become completed when the following conditions are satisfied:
  • The task depends on some master whose execution is completed
  • Each task which depends on the master considered is either already terminated or similarly blocked at a select statement with an open terminate alternative

ACTIONS

  • Consider the following two code fragments
    
    select -- S1            select -- S2   
       accept A;               accept A;    
    or                      else	       
       delay 10.0;            delay 10.0;
    end select;             end select;    
    how does their execution differ?
  • Consider the following two code fragments
    select -- S1            select -- S2   
      accept A;                accept A;    
    or                      else	       
      delay 10.0;              delay 20.0   
      delay 10.0;           end select     
    end select;
    
    how does their execution differ?
  • The following code creates 5 tasks then terminates:
    with Ada.Text_IO; use Ada.Text_IO;
    with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
    procedure Terminate_Test is
       Size : constant Positive := 5;
    
       task type Task_Type is
          entry Start;
       end Task_Type;
    
       task body Task_Type is
       begin
          Put_Line ("Beginning of task");
          accept Start do
             Put_Line ("Start Task ");
          end Start;
          Put_Line ("End of task");
       end Task_Type;
    
       Task_Array : array (Positive range 1..Size) of Task_Type;
    
    begin
       null;
       -- Main task ends here
    end Terminate_Test;
    
    however, since all child tasks are waiting on a rendezvous that never happens the program hangs. Modify the code to include a select/terminate structure such that the code correctly terminates when the main ends even though the child tasks have outstanding rendezvous accept calls. btw: does the main task actually terminate in the example above?
  • In lab3 you used accept to implement a code in which the master thread communicated a number to the first or several child threads. The child thread then added 1 to that number and passed it to the next child. The next child thread receives the incoming number, also adds 1 and then passes the counter to the next child thread. This process is repeated until the "counter" is returned to the first child thread. The first child then returns the counter to the master task. This process is analogous to the lecturer handing a piece of paper to a specific student in a lecture theatre, each student signing the paper and passing it to his/her neighbour until the paper returns to the first student. He/she then hands the paper back to the lecturer.

    Generalize this code so that

    • any of the child processes can receive the initial counter from the master, pass it on and return the final result to the master
    • the whole process can be repeated arbitrary number of times, i.e. the master can hand out one counter (to any child) get the result back and then hand out another counter (possibly to a different child).
    • The master can terminate this process by informing any of the child tasks that it wishes to terminate.

Conditional and Timed Entry Calls

By using select the server task can avoid unreservedly committing itself to accepting a single entry call. Such a facility is not totally available to client tasks, which can only issue a single entry call at any one time. However, Ada does allow the client tasks to avoid committing itself to a single rendezvous by providing conditional and timed entry call facilities.

Timed Entry Calls

A timed entry call issues an entry call which is canceled if the call is not accepted within the specified period (this may be either a relative or absolute time). The syntax is
    timed_entry_call::=
       select
          entry_call_alternative
       or
          delay_alternative
       end select;
NOTE only one entry call and one delay are permitted.

Conditional Entry Call

In a conditional entry call the client withdraws the offer to communicate if the server task is not able to accept the call immediately. It has the same meaning as a timed entry call, where the expiry time is zero: the syntax is:
    timed_entry_call::=
       select
          entry_call_alternative
       else
          sequence_of_statements
       end select;
Note this uses an else rather than an or. If multiple clients issue conditional entry calls to the same server at the same time then only one will succeed, (i.e. the Ada run-time support systems ensures that the commit operation is atomic).


Requeue


The requeue statement is primarily designed to handle two situations
  • After an accept statement or entry body begins execution, it may be determined that the request cannot he satisfied immediately, instead it is necessary to requeue the caller until such a time arises that it can be handled.
  • Part of a request may be handled immediately, but there are additional steps in the process that need to be performed at a later point.
These scenarios often arise in resource allocation problems, e.g. when a client must gather a number of resources before it is able to proceed.

The syntax of requeue is defined as

    requeue_statement ::= requeue entry_name [with abort]
The entry name (called the target entry) may be:
  • another entry within the same protected object, in which case the entry name normally consists only of the identifier naming that entry.
  • an entry name within another protected object, possibly of a different protected type.
  • an entry within a task object.
In all cases the entry named in a requeue statement must either have no parameters or have a parameter profile that is equivalent (i.e. type conformant) with that of the entry (or accept) statement from which the requeue is made.

When a requeue is from one protected object to another then mutual exclusion on the original object is given up once the task is queued. Other tasks waiting to enter the first object will be able to do so. However, a requeue to the same protected object will retain the mutual exclusion lock (if the target entry is open).

The abort clause relates the behaviour of the calling task on timeout or on abort. Specifically once a task has been accepted (or starts to execute the entry of a protected object), then the time-out is canceled and the effect of any abort attempt is postponed until the task comes out of the entry. If the task is requeued, and in the context of time-out, two options could be envisaged:

  • As the first call has been accepted, the time-out should now be canceled
  • If the requeue puts the calling task back on an entry queue, then time-out expiry should again be possible
The equivalent for abort would mean that if the task is again on an entry queue it should be abortable. The requeue statement allows both views to be programmed; the default does not allow further time-outs or aborts, the addition of the with abort clause enable the task to be removed from the second entry.

It is important to appreciate that requeue is not a simple call. If procedure P calls procedure Q, then, after Q has finished, control is passed back to P. In contrast if entry X requeues to entry Y, then control is not passed back to X, Rather after Y has completed, control passes back to the object that called X.

Producer/Consumer: Implementing a Monitor in Ada95

To illustrate the use of requeue we consider a basic producer consumer problem. This problem is outlined in Ben-Ari (and other places). Two types of tasks exist
  • Producers these tasks create a data element which must then be sent to the consumers
  • Consumers these tasks receive a data element and "consume it"!
The basic problem is that a consumer cannot consume until a producer has produced a data element for consumption! An elementary solution is synchronous communication, that is when one task is ready to send and the other is ready to receive the data element is transferred. With a single producer and a single consumer this would be easy to implement in Ada by having the consumer sit in an accept call waiting for the incoming data. If we have more that one consumer it becomes more complicated. A more flexible model is produced by introducing a buffer and allowing asynchronous communications. The producing process then places its product in the buffer while the consuming product removes the data from the buffer. The problem now becomes a matter of managing the intervening buffer. The issues are
  • Access to the buffer must be controlled - we cannot have multiple consumers attempting to retrieve the same element from the buffer (or multiple producers putting different elements in the same place)
  • What happens to the consumers if the buffer is currently empty. It must wait until some data has been produced.
  • What happens to the producer if the buffer is full, it must wait until the buffer has been emptied
From this it should be evident that we cannot control access to the buffer using simple mutual exclusion. What we need is a monitor like construct that allows a task to enter a critical region, examine the buffer and either i) take the contents if it is valid and exit, OR, ii) stall, relinquish exclusive access to the critical region, and wait until it is signaled that the buffer is now full. We can achieve this objective in Ada95 using requeue.

ACTIONS

  • The following code implements a producer/consumer problem. There is a single producer which is the master task. It produces single integer values from 1...N (N=200) that are to then "consumed" by size consumer tasks (size=5). All each consumer does is read the integer variable and sum it into a local.

    with Ada.Text_IO        ; use Ada.Text_IO;
    with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
    
    procedure ProducerConsumer_Test is
    
       Size : constant Positive := 5;
    
       protected type ProducerConsumer is
    
          entry     Get_Data (Value_Get : out Natural);
          procedure Put_Data (Value_Put : in  Natural);
    
       private
          entry Wait_For_Get (Value_Get : out Natural);
    
          Data_Available : Boolean := False;
          The_Data       : Natural;
    
       end ProducerConsumer;
    
       protected body ProducerConsumer is
    
          entry Get_Data (Value_Get : out Natural) when true is
    
          begin
             if Data_Available then
                Value_Get := The_Data;
                Data_Available := False;
             else
                requeue Wait_For_Get;
             end if;
          end Get_Data;
    
          entry Wait_For_Get(Value_Get : out Natural) when Data_Available is
          begin
             Value_Get := The_Data;
             Data_Available := False;
          end Wait_For_Get;
    
          procedure Put_Data(Value_Put : in Natural) is
          begin
             The_Data := Value_Put;
          end Put_Data;
    
       end ProducerConsumer;
    
       PassValue : ProducerConsumer;
    
       task type Consumer is
          entry Supply_Id (Id : in Positive);
       end Consumer;
    
       task body Consumer is
          Consumer_Id,
          My_Value, My_Total : Natural;
          Got_Value          : Boolean := True;
       begin
          accept Supply_Id (Id : in Positive) do
             Consumer_Id := Id;
          end Supply_Id;
          Put("Hello From Consumer "); Put(Consumer_Id); New_Line;
          My_Total := 0;
          while Got_Value loop
             select
                PassValue.Get_Data(My_Value);
                My_Total := My_Total + My_Value;
             or
                -- after 2 seconds and no data we assume it is time to end!
                delay 2.0;
                Got_Value := False;
             end select;
             -- add some delay to permit task swapping
             delay 0.1;
          end loop;
          Put("Consumer:Total ");Put(Consumer_Id);Put(My_Total);New_Line;
       end Consumer;
    
       Consumers : array (Positive range 1..Size) of Consumer;
    
       N : Integer := 200;
    begin
       Put("Hello From the Producer");New_Line;
       -- start consumers
       for i in Consumers'Range loop
          Consumers (i).Supply_Id (i);
       end loop;
    
       Put("Grand Total should be: ");Put(N*(N+1)/2);New_Line;
       for I in 1..N loop
          PassValue.Put_Data(I);
       end loop;
    
    end ProducerConsumer_Test;
    
    

    To implement the buffering between the producer and the consumer we use a protected object, that is accessed by both tasks. Requeue is used to handle the situation when the buffer is either full (producer) or empty (consumer).

    We know from previous labs that the sum of 1..N = N*(N+1)/2, so if we add up the local sums from each consumer we should obtain this value, however, the values of the local sums are of course non-deterministic.

    The code is of course broken (in a few ways). Your task is to fix it! Also define a protected shared natural (take code from last week's lab) and sum the local totals on each child into the shared natural. Have each child print out the value of the shared natural after adding their contribution. The value printed by the last child process to execute these commands should equal the value printed as "Grand Total should be:" on the master.


Task Entry Families


In lab4 you saw that a protected object could declare a family of entries by placing a discrete subtype definition in the specification of the entry declaration. With tasks it is possible to define similar families of entries, however their use is not quite as easy. As an example consider a simple server that wishes to give priority to certain classes of user task:
    type A_Priority is (High, Medium, Low);
    task Server is
       entry Request(A_Priority)(...)
    end Server;
    
a typical call would be
    Server.Request(Low)(...)
    
within the body of the task, a select statement is constructed so that priority is given to calls coming in on the High family entry
    task body server is
      Empty : Boolean;
    begin
      loop
        select
          accept Request(High)(...)do
            ...
          end request
        or
          when Request(High)'count =0 =>
            accept Request(Medium)(...) do
              ...
          end Request;
        or
          when Request(High)'Count   =0 and
               Request(Medium)'Count =0 =>
            accept Request(Low)(...) do
              ...
          end Request;
        or
          terminate;
        end select;
      end loop;
    end Server;
    

ACTIONS

  • Take care of your assignment!
Next Week: Putting it all together...a generic multitasking protected queue.

© The Australian National University
ANU CRICOS Provider Code - 00120C
Monday, 4 September 2006Uwe R. Zimmer

<empty space>