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 2

Ada95: Reusable Programming and Tasks

Generic Units (Spend 30mins of lab time on this)

Last week you wrote several programs to create and manipulate a queue. In the first case integers in the range of 1000 to 40000 were valid members of the queue, while in the second case valid elements were of an enumerated data type labeled "Up", "Down", "Spin" and "Turn". For both cases the actual implementation of the queue was virtually identical.

One of Ada's goals is to facilitate the writing of general-purpose, reusable software components. Generic units make this possible by enabling the user to solve a set of similar, but not identical, problems with a single program unit. Generic units are templates from which several analogous subprograms and packages can be produced without duplication of effort.

A generic template always implicitly contains a blank to be filled with the name of the instance. In addition, there may be explicit blanks standing for values, variables, subprograms, subtypes, and packages. Within the template, explicit blanks are specified by generic formal parameters. Beside specifying a name for the instance, a typical generic instantiation provides generic actual parameters specifying how the explicit blanks are to be filled in.

A generic template consists of two parts: the generic declarations and the generic body. The form of a generic declaration is

generic
   generic formal parameter declaration
   ~~~
   generic formal parameter declaration
unit declaration
													
where the unit declaration is either a subprogram declaration or a package declaration.

As an example we consider a generic implementation of the queue program from lab1. The generic specification and body are as follows:

  • file queue_pack_generic.ads
    generic
       type Element is private;
       
    package Queue_Pack_Generic is
       QueueSize : constant Positive := 10;
       type Queue_Type is limited private;
    
       procedure Enqueue (Item: in  Element; Queue: in out Queue_Type);
       procedure Dequeue (Item: out Element; Queue: in out Queue_Type);
    
       Queueoverflow, Queueunderflow : exception;
    
    private
       type Marker is mod QueueSize;
       type List is array (Marker'Range) of Element;
       type Queue_State is (Empty, Filled);
       type Queue_Type is record
          Top, Free : Marker      := Marker'First;
          State     : Queue_State := Empty;
          Elements  : List;
       end record;
    
    end Queue_Pack_Generic;
    
  • file queue_pack_generic.adb
    package body Queue_Pack_Generic is
       procedure Enqueue (Item: in Element; Queue: in out Queue_Type) is
       begin
          if Queue.State = Filled and Queue.Top = Queue.Free then
             raise Queueoverflow;
          end if;
          Queue.Elements (Queue.Free) := Item;
          Queue.Free := Marker'Pred (Queue.Free);
          Queue.State := Filled;
       end Enqueue;
       Procedure Dequeue (Item: out Element; Queue: in out Queue_Type) is
       begin
          if Queue.State = Empty then
             raise Queueunderflow;
          end if;
          Item := Queue.Elements (Queue.Top);
          Queue.Top := Marker'Pred(Queue.Top);
          if Queue.Top = Queue.Free then
             Queue.State := Empty;
          end if;
       end Dequeue;
    end Queue_Pack_Generic;
    
The above is used in the testing program as follows
  • file queue_test_generic.adb
    with Queue_Pack_Generic;
    with Ada.Text_IO; use Ada.Text_IO;
    procedure Queue_Test_Generic is
       package Queue_Pack_Positive is
          new Queue_Pack_Generic (Element => Positive);
       use Queue_Pack_Positive;
    
       Queue : Queue_Type;
       Item  : Positive;
    begin
       Enqueue (Item => 10, Queue => Queue);
       Enqueue (Item => -1, Queue => Queue);
       Dequeue (Item, Queue);
       Dequeue (Item, Queue);
    exception
       when Queueunderflow => Put("queue underflow");
       when Queueoverflow  => Put("queue overflow");
    
    end Queue_Test_Generic;
    
ACTIONS
  • Compile and run the above code. The code will fail, why? Correct the code.
  • Modify the code to create a second queue that contains at most 6 elements and is used to store the values "Up", "Down", "Spin" and "Turn" of an enumerated data type (i.e. like what you had in last weeks lab!).
  • (Not for the lab - but try and modify your code so that you also give the size of the queue as a generic formal parameter. This is not straightforward because of the modular data type!)

Type Extension

Often it is desirable to extend an existing type and create a derived type whose values contain all the information of the parent type plus some additional information corresponding to the special characteristics of the derived type. It may also be necessary to redefine or augment the methods associated with the original type.

In Ada not every type can serve as the parent of an extension. Only certain types known as "tagged" types can be extended. As an example we will extend the queue package with a new procedure that permits us to read the content of the queue without actually dequeuing an element. We start by modifying the basic queue package to permit it to be extended. This is done by changing the queue_type record to be ":tagged":

  • File queue_pack_object_base.ads
    package Queue_Pack_Object_Base is
       QueueSize : constant integer := 10;
       type Element is new Positive range 1..1000;
       type Marker is mod QueueSize;
       type List is array (Marker'Range) of Element;
       type Queue_State is (Empty, Filled);
       type Queue_Type is tagged record
          Top, Free : Marker      := Marker'First;
          State     : Queue_State := Empty;
          Elements  : List;
       end record;
       procedure Enqueue (Item: in  Element; Queue: in out Queue_Type);
       procedure Dequeue (Item: out Element; Queue: in out Queue_Type);
    
       Queueoverflow, Queueunderflow : exception;
    
    end Queue_Pack_Object_Base;
    
  • File queue_pack_object_base.adb
    package body Queue_Pack_Object_Base is
       procedure Enqueue (Item: in Element; Queue: in out Queue_Type) is
       begin
          if Queue.State = Filled and Queue.Top = Queue.Free then
             raise Queueoverflow;
          end if;
          Queue.Elements (Queue.Free) := Item;
          Queue.Free := Marker'Pred (Queue.Free);
          Queue.State := Filled;
       end Enqueue;
       Procedure Dequeue (Item: out Element; Queue: in out Queue_Type) is
       begin
          if Queue.State = Empty then
             raise Queueunderflow;
          end if;
          Item := Queue.Elements (Queue.Top);
          Queue.Top := Marker'Pred(Queue.Top);
          if Queue.Top = Queue.Free then
             Queue.State := Empty;
          end if;
       end Dequeue;
    end Queue_Pack_Object_Base;
    
To read an element from the queue we need to define a pointer telling us which element to read and a new function to read the element:
  • File queue_pack_object.ads
    with Queue_Pack_Object_Base; use Queue_Pack_Object_Base;
    package Queue_Pack_Object is
       type Ext_Queue_Type is new Queue_Type with record
         Reader       : Marker      := Marker'First;
         Reader_State : Queue_State := Empty;
       end record;
       procedure Read_Queue(Item: out Element; Queue: in out Ext_Queue_Type);
    end Queue_Pack_Object;
    
  • file queue_pack_object.adb
    package body Queue_Pack_Object is
       procedure Read_Queue(Item: out Element; Queue: in out Ext_Queue_Type) is
       begin
          if Queue.Reader_State = Empty then
             raise Queueunderflow;
          end if;
          Item := Queue.Elements(Queue.Reader);
          Queue.Reader := Queue.Reader -1;
          if Queue.Reader = Queue.Free then Queue.Reader_State := Empty; end if;
       end Read_Queue;
    end Queue_Pack_Object;
    
We then write a small program to test the new package:
  • File queue_test_object.adb
    with Queue_Pack_Object_Base; use Queue_Pack_Object_Base;
    with Queue_Pack_Object; use Queue_Pack_Object;
    with Ada.Text_Io; use Ada.Text_Io;
    with Ada.Integer_Text_Io; use Ada.Integer_Text_Io;
    
    procedure Queue_Test_Object is
       Queue : Ext_Queue_Type;
       Item  : element;
    begin
       Enqueue (Item => 1, Queue => Queue);
       Read_Queue(Item, Queue);
       Ada.Integer_Text_Io.Put(Integer(Item));
       Enqueue(Item => 5, Queue => Queue);
       Dequeue (Item, Queue);
       Ada.Integer_Text_Io.Put(Integer(Item));
       Dequeue (Item, Queue);
       Ada.Integer_Text_Io.Put(Integer(Item));
    exception
       when Queueunderflow => Put("queue underflow");
       when Queueoverflow  => Put("queue overflow");
    
    end Queue_Test_Object;
    

ACTIONS

  • Build the above code and run it. Again you will find that it does not work correctly. Determine what is wrong and fix it!!
  • The above read function works from the head of the queue to the tail. Modify this so that we have two functions that share the same pointer, but one function moves the pointer from the head of the queue to the tail, while the other moves the pointer from the tail to the head. Both functions should return the element that the pointer refers to before incrementing/decrementing it..
  • (At home or later) Re-write the above using generic units.

Tasks (Where the real material starts)

Having mastered Ada we are now ready for some concurrency!! In Ada each task corresponds to a data object, called a task object. Associated with each task object is a sequence of statements to be executed, a task to execute them, a set of variables for use only by this task, and an interface for synchronization and communication with other tasks.

Like other data objects, task objects belong to types, in this case task types. All the task objects in the same task type share certain characteristics:

  • Their tasks independently execute the same sequence of statements.
  • Their variables are described by the same declarations, although each task object has its own copy of these variables.
  • They have the same interface for communication with other tasks,
Task types are limited! That is task types cannot be copied or tested for equality.

Task types are not defined in the same way as the types we have encountered until now. Two steps are required - a task type declaration and a task body. The task type declaration specifies the entries of the tasks in the task type. The task body contains the statements executed by tasks in the task type and the declarations of types, objects, and other entities used in those statements. The task-type declaration and task body together are called a task unit.

The typical form of a task-type declaration is

    task type identifier [discriminant-part] is
       entry-declaration
       ~~~
       entry-declaration
    end identifier;
In effect, discriminants of a task object act as parameters controlling the behavior of the associated task. Entry points are where the task begins execution. For the moment we will only use the more succinct form of task-type declaration:
    task type identifier
A task body looks like a subprogram body, except for the heading:
    task body identifier is
       declarative part
    begin
       sequence-of-statements
    exception
       handler
       ~~~
       handler
    end identifier;
Some time after a task object is created - either by an object declaration or by the evaluation of an allocator - the declarative part of the task body is elaborated and the sequence of statements is executed concurrently with the other tasks. Any variables created during elaboration of the task body's declarative part belong to that task object alone.

Note, although each task executes the sequence of statements in its task body, these statements often consist of calls on subprograms outside the task body, e.g. subprograms provided by library packages.

We start by creating two tasks and having them each print out "hello world"

  • File two_tasks.adb
    with Ada.Text_IO; use Ada.Text_IO;
    procedure Two_Tasks is
       task First_one;
       task Second_one;
       task body First_one is
       begin
           Put( "Hello World from Task 1 ");
           New_Line;
       end First_One;
       task body Second_One is
       begin
           Put( "Hello World from Task 2 ");
           New_Line;
       end Second_One;
    begin
       null;
    end Two_Tasks;
    
    																
    
    															
    															
    														
    														

ACTIONS

  • Compile and run the above code.
You should find that the above code dutifully prints out first and second task (you might have to use "Ctrl C" to stop the job) The problem is, how do we know that these tasks are running concurrently? We could just as well have reproduced the above output from a standard sequential program!

When dealing with concurrent programs it is often useful to have your program printout timestamps, i.e. print the current wall clock time. By comparing dates and times you can deduce the order in which events took place. Below is a simple Ada program that uses the Ada.Calendar package to do this:

  • File timestamp.adb
    with Ada.Text_IO; use Ada.Text_IO;
    with Ada.Calendar;
    procedure Timestamp is
       Year    : Ada.Calendar.Year_Number;
       Month   : Ada.Calendar.Month_Number;
       Day     : Ada.Calendar.Day_Number;
       Seconds : Ada.Calendar.Day_Duration;
    begin
       for I in 1..10 loop
          Ada.Calendar.Split(Ada.Calendar.Clock, Year, Month, Day, Seconds );
          Put( " Year   " & Year'img );
          Put( " Month  " & Month'img );
          Put( " Day    " & Day'img );
          Put( " Second " & Seconds'img );
          New_Line;
          delay 1.0;
       end loop;
    end Timestamp;
    
ACTIONS
  • Compile and run the above code.
  • The values printed are obvious, except for "seconds". What does seconds correspond to?
  • What does delay do and what are its units? (Try modifying the value and see how the output changes.)
  • Now return to the two_tasks program and have each task print out the time. Play around with the value of the delay parameter. You should convince yourself that you do have first and second task running concurrently.
  • For the two task program set the delay to 0.0. What happens? What happens if you remove the delay entirely (or comment it out). In Ada a delay of 0 has a specific meaning - what is it? (Ask your tutor if you are at all unsure).
  • In the two task program, how many tasks are there? The answer is NOT two. Show this to be true.
Finally consider the following program. It creates two tasks that either add or subtract one from a variable sum depending on its current value:
  • file counter_test.adb (sources)
    
    with Ada.Text_IO        ; use Ada.Text_IO;
    with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
    
    procedure Counter_Test is
    
       Sum : Natural := 50;
    
       task type Counter (Goal : Natural);
    
       task body Counter is
    
       begin
          for I in 1..100 loop
             if Sum > Goal then
                Sum := Sum - 1;
             elsif Sum < Goal then
                Sum := Sum + 1;
             end if;
             delay 0.0; -- try leaving out this delay statement!
          end loop;
          
          Put( "Final value in Counter (task)"); Put(Sum); New_Line;
       end Counter;
    
       Counter_1 : Counter (40);
       Counter_2 : Counter (70);
    
    begin
       Put( "Final value in Counter_Test "); Put(Sum); New_Line;
    end Counter_Test;
    
ACTIONS
  • Predict what you think will happen when you run the above code. Then run it and see if you are right.
  • Will the result be the same if you delete the 'delay' statement?
  • Run the code several times. Does it always give the same output? (A convenient shell command is repeat - eg try typing "repeat 10 counter_test")
  • Above you might have predicted that at least one of the two tasks would have printed out a value of zero for sum. You probably found this was not the case - indeed this does not have to be the case. Make sure you understand why (this is not so simple).
  • Modify the above code so that sum is a local variable within each task? (Make sure you understand the difference between a local and shared variable - this is of fundamental importance.)
  • Similar to above, have a program that creates two tasks. Have task 1 add one to the value of a shared variable (sum) while task 2 subtract one from the variable. Initialise the variable sum to zero. Have the tasks alternate, with task 1 adding one first then task 2 subtracting one, then task 1 adding one etc with each task adding or subtracting a number 10 times. When each task modifies the value of sum have it print out an identifier (i.e. "I'm task 1"), the value of the variable and the current time. You should achieve the above using only access to shared variables (you may want to consider your lecture notes).

Next Week

More on tasks and task rendezvous.


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

<empty space>