|
Laboratory 3
Ada95: Tasks and Task Rendezvous
Tasks
You were introduced to tasks in last week's lab. The full syntax for the task type is as follows:
task_type_declaration ::=
task type identifier [discriminant-part] [is task_definition];
task_definition ::=
{task_item}
[private
{task_item}]
end identifier;
task_item ::= entry_declaration | representation_clause
The task body is declared as follows:
task_body ::=
task body identifier is
declarative part
begin
sequence-of-statements
exception
handler
~~~
handler
end identifier;
The full declaration of a task type consists of its specification and body. The specification contains:
- The type name
- A discriminant part - this defines the discrete or access parameters that can be passed to instances of the task type at their creation time
- A visible part - this defines the task types entries and representation clauses which are visible to the user of the task type, it also includes the discriminant part
- A private part - this defines the task types entries and representation clause which are invisible to the user of the task type
As you will see later in this weeks lab the entries define those parts of a task that can be accessed from other tasks; they indicate how other tasks can communicate directly with the task. Representation clauses are intended solely for interrupt handling.
Example specifications are:
task type Course_Convener;
-- this task type has no entries; no other task can
-- communicate directly with tasks created from this task
task type Lecturer(Who: Name);
-- this task type has no entries but task objects are
-- passed the name of the lecturer at creation time
task type Student(uid : uid := -999999);
-- objects of this task type will allow communication via
-- two entries the number of the student is passed to the
-- task at creation time; if no value is passed, a value of -999999
-- is used
entry do_Laboratory(Lab_No : Positive);
entry do_assignment(Assign_No : Positive);
end student
Task Creation, Reference and Data Scoping
A task type can be regarded as a template from which actual tasks are created. All objects of a given task type execute the same series of statements, with their own copies of the local data. They can also manipulate data which is in scope. From last weeks lab you should be familiar with how to define local and global data.
The following code dynamically creates a number of tasks and assigns them a unique id. Three different versions of task creation from task types are shown. The first is creation by individual declaration, the second one is by array declaration and following entry-call, the last one is via pointers and 'new' (which will be avoided whenever possible, as it gives you the least amount of control and is the most error prone - nevertheless necessary in cpecific situations). The entry call used here will be introduced in more detail below, but you can guess what it does already, can't you?
- File dynamic.adb
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Calendar;
procedure Dynamic is
subtype Task_Range_1 is Positive range 1..3;
subtype Task_Range_2 is Positive range 6..9;
task type Task_w_Discriminant (Id : Positive);
task body Task_w_Discriminant is
Year : Ada.Calendar.Year_Number;
Month : Ada.Calendar.Month_Number;
Day : Ada.Calendar.Day_Number;
Seconds : Ada.Calendar.Day_Duration;
begin
Put("Hello From Task "); Put( Id); New_Line;
for I in 1..10 loop
Ada.Calendar.Split(Ada.Calendar.Clock, Year, Month, Day, Seconds);
Put("Task "); Put(Id, 3); Put(" " & Seconds'img ); New_Line;
delay 1.0;
end loop;
end Task_w_Discriminant;
task type Task_w_Entry is
entry Receive_Task_Id (Task_Id : in Positive);
end Task_w_Entry;
task body Task_w_Entry is
Year : Ada.Calendar.Year_Number;
Month : Ada.Calendar.Month_Number;
Day : Ada.Calendar.Day_Number;
Seconds : Ada.Calendar.Day_Duration;
Id : Positive;
begin
accept Receive_Task_Id (Task_Id : in Positive) do
Id := Task_Id;
end Receive_Task_Id;
Put("Hello From Task "); Put( Id); New_Line;
for I in 1..10 loop
Ada.Calendar.Split(Ada.Calendar.Clock, Year, Month, Day, Seconds);
Put("Task "); Put(Id, 3); Put(" " & Seconds'img ); New_Line;
delay 1.0;
end loop;
end Task_w_Entry;
Individual_Task_20 : Task_w_Discriminant (20);
Individual_Task_30 : Task_w_Discriminant (30);
Task_Array : array (Task_Range_1) of Task_w_Entry;
type Task_w_Discriminant_Ptr is access Task_w_Discriminant;
Task_Ptr_Array : array (Task_Range_2) of Task_w_Discriminant_Ptr;
begin
for i in Task_Array'Range loop
Task_Array (i).Receive_Task_Id (i);
end loop;
for i in Task_Ptr_Array'Range loop
Task_Ptr_Array (i) := new Task_w_Discriminant (i);
end loop;
end Dynamic;
The Task Lifecycle
A task is said to be created by its elaboration. The execution of a task object has three main phases:
- Activation - the elaboration of the declarative part, if any, of the task body (any local variables in the body of the task are created and initialized during activation).
- Normal execution - the execution of the statements within the body of the task
- Finalization - the execution of any finalization code associated with any objects in its declarative part
A task, in general, indicates its willingness to begin finalization by executing its end statement. A task may also begin its finalization as a result of an unhandled exception, or by executing a select statement with a terminate alternative (see later) or by being aborted. A finished task is called completed or terminated depending on whether it has any active dependents.
Task Activation
For static tasks, activation starts immediately after the complete elaboration of the declarative part in which they are defined. The following should be noted
- All static task created within a single declarative region begin their activations immediately the declarative region has completed elaboration (i.e. after begin, but before any statement following begin).
- The statement following the declarative region is not executed until all task have finished their activation.
- Following activation the execution of the task object is defined by the appropriate task body
- A task need not wait for the activation of other concurrently created tasks before executing its body
- A task may attempt to communicate with another task which although created has not yet been activated. The calling task will be delayed until the communication can take place
Dynamic tasks are activated immediately after the evaluation of the allocator (the new operator) which creates them.
Program errors can lead to exceptions being raised during the initial phases of a task's existence. If an exception is raised in the elaboration of a declarative part, then any task created during that elaboration becomes terminated and is never activated. As the task itself cannot handle the exception, the Ada Language models requires that the parent (creator) task deal with the situation: the predefined exception Tasking_Error is raised.
Task Hierarchies
A task that is directly responsible for creating another task is called the parent of that task, while the task that is created is called the child.
When a parent creates a child, the parents execution is suspended while it waits for the child to finish activation. Once the child has finished activation the parent and child proceed concurrently.
If a child task creates another task during activation then the original parent (now grandparent) must wait for both children to finish activation.
While the parent of a child task is responsible for the creation of that task, the master of that task must wait for it to terminate. With static task creation the parent and the master are the same, however, with dynamic task creation the master of the task is the declarative region which contains the access type definition. This may or may not be where the allocator is evaluated.
When the master task has completed its execution and is waiting for its dependent children to complete it is said to be completed, but not terminated.
ACTIONS
- Consider the following outline of an Ada program
declare -- some declarative regions executed by task parent
task type T_type;
type Prt_T_type is access T_type;
A1 : T_Type; -- creation of A1
A2 : T_Type; -- creation of A2
task body T_Type is ..
begin -- activation of A1, A2
Block_in_master:
declare
B : T_Type; -- Creation of B
C : Prt_T_Type := new T_Type; -- creation, activation of C.All
D: Prt_T_Type;
begin -- activation of B
D := new T_Type; - creation, activation of D.all
end Block_in_master;
end;
In what order are the tasks created and activated in the above code? (I am expecting you to outline 8 phases).
- Why can you not allow the master task to terminate before the child? (Verify with tutor in lab).
- Write an Ada program that takes as input a positive integer of value N and computes explicitly the sum of all integers from 1 to N, i.e. 1+2+3+4+5+6...N. The program should verify that it has computed the correct result by comparing the computed value with the expected value of N*(N+1)/2. (But how do I read in a value you ask - guess, then ask your tutor! If there is no tutor, hard code a value, then fix this problem later.)
- Now modify your code to create up to 20 child tasks, with the exact number of tasks created determined at run time by an input parameter. Have each child task compute a distinct sub-range of the explicit sum, e.g. if N=6 and there are two tasks then task 1 computes 1+2+3, while task 2 computes 4+5+6. Have the parent task sum the different task sums and print out the final result. Be sure that the parent compares the value computed with that obtain from the formula given above. Your program should only use access to shared variables to control synchronization. (That is have tasks busy wait until the value of a shared variable is changed. Be sure to include a delay 0.0 to permit task swapping. If this is unclear you need to revise the last part of lab2!)
- Verify your program with your tutor.
Task Rendezvous
So far the only communication we have used between tasks is via shared variables. The rendezvous is a more elaborate construct that permits direct communication between tasks. One task, the server, declares a set of services that it is prepared to offer the other tasks - the clients. This is done through declaring one or more public entries in the task specification (this is what the entries are for). A client task issues an entry call on the server task by identifying both the server and the required entry. The server task indicates a willingness to provide the service at any particular time by issuing an accept statement.
For communication to take place both the client and the server must have issued their respective requires. When they have, the communication takes place, this is called a rendezvous.
The Accept Statement
An accept statement specifies the actions to be performed when an entry is called and the formal part of the accept statement must conform exactly to the formal part of the corresponding entry. In general the accept statement has the following form:
accept Entry_Name(family_Index)(P: Parameters) do
sequence of statements
exception
handler
~~~
handler
end Entry_Name;
The sequence of statements in an accept statement may contain subprogram calls, entry calls, protected object calls, accept statements, return call, etc. If the accept statement encounters a return it has the effect of terminating the accept statement and returning control to the calling task.
Since the calling task is unable to proceed until execution of the accept statement is complete, the sequence of statements in the accept statement should be kept as short as possible. Any actions that could be performed after the accept statement rather than inside it should be moved out. Thus an accept statement with no body has the effect of synchronizing two tasks.
ACTIONS
- The following program attempts to use rendezvous to synchronize all child tasks with the parent. Specifically the intention is that no child task will begin to execute the "for i in 1..10 loop" until all child tasks have reached that point. Similarly it is intended that no child task terminates until all child tasks have finished the aforementioned loop. Unfortunately the synchronization is not working correctly. Run the program and verify that it is indeed true. Then fix it.
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Calendar;
procedure Synchronize is
task type Task_Type is
entry Receive_Task_Id (Task_Id : in Positive);
entry Start;
entry Stop;
end Task_Type;
task body Task_Type is
Year : Ada.Calendar.Year_Number;
Month : Ada.Calendar.Month_Number;
Day : Ada.Calendar.Day_Number;
Seconds : Ada.Calendar.Day_Duration;
Wait : Float;
Id : Positive;
begin
accept Receive_Task_Id (Task_Id : in Positive) do
Id := Task_Id;
end Receive_Task_Id;
Put("Hello From Task "); Put( Id); New_Line;
Wait := Float(Id)*1.0;
accept Start do
Put("Starting Task "); Put( Id); New_Line;
end Start;
for I in 1..5 loop
Ada.Calendar.Split(Ada.Calendar.Clock, Year, Month, Day, Seconds);
Put("Task "); Put(Id); Put(" " & Seconds'img ); New_Line;
delay Duration(Wait);
end loop;
accept Stop do
Put("Stopping Task "); Put( Id); New_Line;
end Stop;
end Task_Type;
Task_Array : array (Integer range 1..2) of Task_Type;
begin
for I in Task_Array'Range loop
Task_Array(I).Receive_Task_Id (I);
end loop;
for I in Task_Array'Range loop
Task_Array(I).Start;
end loop;
for I in Task_Array'Range loop
Task_Array(I).Stop;
end loop;
end;
The following code is designed to create a number of child tasks and then initiate a ring of synchronizations, i.e. synchronizations that go parent-child0, child0-child1, child1-child2, child2-child3, child3-child4, child4-parent. At each synchronization point the value of a counter is incremented before being passed from task to task, thus in the following the parent should receive back a value for the counter of 6. Unfortunately the code is not complete. Finish it!
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Ring is
type Ring_Range is mod 5;
task type Task_Type is
entry Receive_Task_Id (Task_Id : in Ring_Range);
entry Start;
entry Ring (Count_In : in Natural);
entry Stop;
end Task_Type;
Task_Array : array (Ring_Range) of Task_Type;
task body Task_Type is
Counter : Natural := 0;
Towhom : Ring_Range;
Id : Ring_Range;
begin
accept Receive_Task_Id (Task_Id : in Ring_Range) do
Id := Task_Id;
end Receive_Task_Id;
Put("Hello From Task "); Put(Integer (Id), 2); New_Line;
delay 1.0;
-- first task synchronize with parent
if Id = Task_Array'First then
accept Start;
end if;
-- synchronize with neighbour
Towhom := Id + 1;
if Id = Task_Array'First then
Task_Array (Towhom).Ring(Counter);
accept Ring (Count_In : Natural) do
Counter := Count_In + 1;
end Ring;
else
accept Ring (Count_In : Natural) do
Counter := Count_In + 1;
end Ring;
Task_Array (Towhom).Ring(Counter);
end if;
Put ("Task "); Put (Integer (Id), 2); Put (" counts "); Put (Counter, 2); New_Line;
-- first task synchronize with parent
if Id = Task_Array'First then
accept Stop;
end if;
end Task_Type;
begin
-- start children
for I in Task_Array'Range loop
Task_Array(I).Receive_Task_Id (I);
end loop;
-- tell first child to start
Task_Array (Task_Array'First).Start;
end Ring;
Now re-write the multi-task summation program that you developed above to use rendezvous constructs to communicate between the parent and child tasks.
Next Week
Select, synchronization, protected objects.
|