|
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:
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"
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:
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.
|