Friday, October 23, 2020

Tasking and Ada Rendezvous

 

Tasking and the Ada Rendezvous

Multiprocessing has been a part of the Ada programming language since the language was first standardized in 1983. The original version of Ada provided active concurrency objects called tasks. Today tasks are commonly implement by Ada compilers as operating system threads, but the Ada language does not depend on operating system threading to implement tasks.

The execution of an Ada program consists of the execution of one or more tasksEach task represents a separate thread of control that proceeds independently and concurrently between the points where it interacts with other tasks. The various forms of task interaction are described in this clause, and include:

·         the activation and termination of a task;

·         a call on a protected subprogram of a protected object, providing exclusive read-write access, or concurrent read-only access to shared data;

·         a call on an entry, either of another task, allowing for synchronous communication with that task, or of a protected object, allowing for asynchronous communication with one or more other tasks using that same protected object;

·         a timed operation, including a simple delay statement, a timed entry call or accept, or a timed asynchronous select statement (see next item);

·         an asynchronous transfer of control as part of an asynchronous select statement, where a task stops what it is doing and begins execution at a different point in response to the completion of an entry call or the expiration of a delay;

·         an abort statement, allowing one task to cause the termination of another task.

Tasks can be implemented as a task type, allowing multiple identical instances of a task to be created, or as single task entities defined as members of an anonymous task type.

Definition of a task type includes declaration of all interfaces to the task type. Task interfaces, or calling points, are called task entries. Each task entry allow another task to call an instance of the task type, possibly passing data to or from the task type instance.

Rendezvous

When one task calls an entry of another task the two task synchronize to allow data to be passed from one task to the other. If task A calls an entry named set_id declared in task B with the purpose of passing an id value from task A to task B the two tasks synchronize to allow direct communication between the tasks. Task A calls the Task B entry. Task B accepts the entry call. Synchronization happens when both task are ready to process the entry. If task A calls the entry before task B accepts the entry call then task A suspends until task B is ready. Similarly, if task B accepts the entry before any task calls the entry then task B suspends until the entry is called. An entry is allowed to transfer data to or from the task calling the entry.

Table 1 Task Type Example

task type Counter is
   entry Set_Num (Val : in Integer);
end Counter;

 

The task type in the example above is named Counter. Task type Counter has one entry. A task type may have multiple entries or it may have no entries.

The single entry in this example is named Set_Num. Set_Num requires a single parameter of type Integer. The “in” mode designation causes the value to be passed from the calling task to the called instance of Counter.

Each task or task type declaration must be completed with a task body. The task or task type declaration contains the declaration of the name of the task or task type as well as the declaration of all entries associated with the task or task type. The task body contains the logic implemented by the task. The task body must have the same name as its corresponding task or task type declaration.

Table 2 Task Body Example

task body Counter is
   Seed : Generator;
   Num  : Integer;
begin
   Reset (Seed);

   accept Set_Num (Val : in Integer) do
      Num := Val;
   end Set_Num;
 

   delay Duration (Random (Seed) * 0.01);

   Put_Line (Num'Image);

end Counter;

 

The structure of a task body is very similar to the structure of an Ada procedure. Local variables are declared between the “is” reserved word and the “begin” reserved word. The “accept” statement performs the task’s interaction with the entry call. In this case the parameter “Val” contains the value passed from the task calling the entry. That value is copied into the local variable “Num”. The accept operation ends when the code reaches “end Set_Num”. Upon completion of the accept operation the synchronization of this task with its calling task completes and both tasks resume asynchronous execution.

Calling a Task Entry

The following example demonstrates how one task calls the entry of another task.

Table 3 Calling a Task Entry

with Ada.Text_IO;               use Ada.Text_IO;

with Ada.Numerics.Float_Random; use Ada.Numerics.Float_Random;

procedure Main is

   task type Counter is
      entry Set_Num (Val : in Integer);
   end Counter; 

   task body Counter is
      Seed : Generator;
      Num  : Integer;

   begin

      Reset (Seed);

      accept Set_Num (Val : in Integer) do
         Num := Val;
      end Set_Num; 

      delay Duration (Random (Seed) * 0.01);

      Put_Line (Num'Image);

   end Counter;

   subtype Index is Integer range 0 .. 9;

   Counters : array (Index) of Counter;

begin

   for I in Counters'Range loop
        Counters (I).Set_Num (I);
   end loop;

end Main;

 

The purpose of this simple program is to use tasks to print the digits 0 through 9 in random order. The task type counter is designed to receive a single integer value through its Set_Num entry, delay execution for a random time (in this case the time is a random fraction of milliseconds between 9 milliseconds and 1 millisecond) and then output the value of its local variable named Num. Upon completing the output operation the instance of Counter completes.

An array of Counter objects is created. The name of that array is Counters. The array is indexed by the values 0 through 9, which causes the array to have 10 instances of task type Counter. Those tasks start automatically when program execution reaches the “begin” statement of the Main procedure.

Looking back at the task body for Counter we see that the first action of the task is to call the Reset procedure for random generator, setting the random number seed to a unique value based upon the current computer clock. After resetting the random number seed the task calls the accept operator for the Set_Num entry. The task then suspends until its Set_Num entry is called.

The “for” loop in the Main procedure iterates through the array of Counter tasks, calling each one in order and passing its array index value as the number it will output. Keep in mind that the Main procedure executes in a task separate from the ten Counter tasks.

Another example of the Ada Rendezvous is shown below. This example defines an Ada package implementing parallel addition of an array of integer values. The example is implemented in three files.

Table 4 Parallel_Addition.ads

package Parallel_Addition is

   type Data_Array is array(Integer range <>) of Integer;

   type Data_Access is access all Data_Array;

   function Sum(Item : in not null Data_Access) return Integer;

end Parallel_Addition;

 

This package specification declares an unconstrained array type named Data_Array. Data_Array arrays are indexed by some set of values within the valid set of Integer values. Each element contains an Integer. An unconstrained array allows the user to pass an array of any size permitted by the computer hardware.

The type Data_Access is a reference to Data_Array.

The function Sum takes a parameter of type Data_Access so that very large arrays created on the heap may be passed to the function. The function returns a single Integer which is the sum of all the elements in the array passed to the function.

Table 5 Parallel_Addition.adb

package body Parallel_Addition is

   ---------
   -- Sum --
   --------- 

   function Sum (Item : in not null Data_Access) return Integer is

      task type Adder is

         entry Set (Min : Integer; Max : Integer);

         entry Report (Value : out Integer);

      end Adder;

 

      task body Adder is

         Total : Integer := 0;
         First : Integer;
         Last  : Integer;

      begin

         accept Set (Min : Integer; Max : Integer) do
            First := Min;
            Last  := Max;
         end Set;

         for I in First .. Last loop
            Total := Total + Item (I);
         end loop;

         accept Report (Value : out Integer) do
            Value := Total;
         end Report;

      end Adder;

      A1  : Adder;
      A2  : Adder;
      R1  : Integer;
      R2  : Integer;
      Mid : constant Integer := (Item'Length / 2) + Item'First;

   begin

      A1.Set (Min => Item'First, Max => Mid);
      A2.Set (Min => Mid + 1, Max => Item'Last);

      A1.Report (R1);
      A2.Report (R2);

      return R1 + R2;

   end Sum;

end Parallel_Addition;

 

The package body for Parallel_Addition contains the implementation of the Sum function declared in the Parallel_Addition package specification.

Within the Sum function we find the declaration of a task type name Adder. Instances of Adder will perform the parallel addition. The task type declaration for Adder includes two entries named Set and Report. The Set entry sets the minimum and maximum index values to be used by the task. The Report entry passes the total calculate by the task back to the entry’s calling task.

Two instances of Adder are created. One is named A1 and the other is named A2. Similarly two integer variables intended to hold the results of the additions are created. Those variables are named R1 and R2.

The Sum function calculated a middle value for the index values in the array passed to it as Item.

The Adder tasks begin execution when program execution reaches the “begin” of the Sum function.

The Sum function calls the Set entry for Adder A1 passing it the index values for the first index of Item and the Mid value. Similarly, the set entry for Adder A2 is passed the index value of Mid + 1 and the last index value of Item.

The Sum function immediately calls the Adder A1 Report entry to receive the value for R1, followed by calling the Adder A2 report entry to receive the value for R2. The Sum function will suspend until A1 accepts its Report entry. It will then suspend again if necessary until A2 accepts its Repot entry.

Sum returns the sum of R1 and R2.

Table 6 Parallel_Addition_Test.adb

with Parallel_Addition; use Parallel_Addition;
with Ada.Text_IO;       use Ada.Text_IO;
with Ada.Calendar;      use Ada.Calendar; 

procedure Parallel_Addition_Test is

   The_Data : Data_Access := new Data_Array (1 .. Integer'Last);

   Start    : Time;
   Stop     : Time;

   The_Sum  : Integer; 

begin

   The_Data.all := (others => 1);
   Start        := Clock;
   The_Sum      := Sum (The_Data);
   Stop         := Clock;
   Put_Line ("The sum is: " & Integer'Image (The_Sum));
   Put_Line
     ("Addition elapsed time is " &
      Duration'Image (Stop - Start) &
        " seconds.");
   Put_Line
     ("Time per addition operation is " &
          Float'Image(Float(Stop - Start) / Float(The_Data'Length)) &
        " seconds.");

end Parallel_Addition_Test;

 

The program Parallel Addition Test creates an array containing 2,147,483,647 elements. The object of this test is to calculate an average time for each addition operation using the Parallel_Addition package. A large array provides a statistically solid number for the average time.

In this case all the elements of the array are initialized to 1 so that the addition will not result in an integer overflow. This also gives a good check of the addition, since the correct result is 2,147,483,647.

The program captures the clock time in the variable Start, calls the Sum function, then captures the time in the variable Stop. The difference between the two times is the approximate time used to sum the array.

The addition result along with the elapsed time and the average time per addition operation are reported.

The result of an example run is:

Table 7 Example Parallel_Addition_Test Output

The sum is:  2147483647
Addition elapsed time is  4.918378900 seconds.
Time per addition operation is  2.29030E-09 seconds.

 

No comments:

Post a Comment

Comparison of three algorithms for summing an array of integers

This article shares a comparison of the time taken to sum an array of integers. This article tests three algorithms for summing the array. •...