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.

 

Wednesday, October 21, 2020

Is Ada truly verbose?

 

People who prefer programming language syntax derived from the C language have often argued that the Ada language is verbose because it uses entire words rather than punctuation.

Meaning

C Syntax

Ada Syntax

Begin a block

{

begin, loop

End a block

}

end, end if, end loop, end subprogram name, end task name, end protected object name, end record

Declare a function taking a single integer parameter and returning an integer value

int foo (int x);

function foo (x : integer) return integer;

Declare a 10 element integer array indexed with the values 0 through 9

int a [10];

a : array (0..9) of integer;

Write a “for” loop to sum all the elements in array a declared above.

for (int I = 0; I < 10; i++)

{

   sum += a [i];

}

for I in a’range loop

   sum := sum + a (i);

end loop;

 

Of course, not everything in Ada takes more lines of code or more typing than the equivalent program in C or C++. For instance, the following C++ program is taken from the CodesCracker website. Its purpose is to convert a user input of a hexadecimal value into the corresponding decimal value.

/* C++ Program - Hexadecimal to Decimal Conversion */            

#include<iostream.h>

#include<stdlib.h>

#include<conio.h>

#include<math.h>

unsigned long convtodecnum(char hex[]);

void main()

{

    clrscr();

    unsigned long decnum;

    char hex[9];     // 8 characters for 32-bit Hexadecimal Number and one for ' '   

    cout<<" Enter 32-bit Hexadecimal Number : ";

    cin>>hex;

    decnum = convtodecnum(hex);

    cout<<"Value in Decimal Number is "<<decnum<<"\n";

    getch();

}

unsigned long convtodecnum(char hex[])

{

    char *hexstr;

    int length = 0;

    const int base = 16;     // Base of Hexadecimal Number

    unsigned long decnum = 0;

    int i;

    // Now Find the length of Hexadecimal Number

    for (hexstr = hex; *hexstr != '\0'; hexstr++)

    {

       length++;

    }

    // Now Find Hexadecimal Number

    hexstr = hex;

    for (i = 0; *hexstr != '\0' && i < length; i++, hexstr++)

    {

       // Compare *hexstr with ASCII values

       if (*hexstr >= 48 && *hexstr <= 57)   // is *hexstr Between 0-9

       {

           decnum += (((int)(*hexstr)) - 48) * pow(base, length - i - 1);

       }

       else if ((*hexstr >= 65 && *hexstr <= 70))   // is *hexstr Between A-F

       {

           decnum += (((int)(*hexstr)) - 55) * pow(base, length - i - 1);

       }

       else if (*hexstr >= 97 && *hexstr <= 102)   // is *hexstr Between a-f

       {

           decnum += (((int)(*hexstr)) - 87) * pow(base, length - i - 1);

       }

       else

       {

           cout<<"Invalid Hexadecimal Number \n";

       }

    }

    return decnum;

}

 

An Ada program handling the same input and results in the same output is:

-- Convert Hexadecimal string to decimal value

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

   type Unsigned_Long is mod 2**32;

   Decimal : Unsigned_Long;

begin

   Put ("Enter a 32 bit hexadecimal number: ");

   Decimal := Unsigned_Long'Value ("16#" & Get_Line & "#");

   Put_Line (Decimal'Image);

exception

   when Constraint_Error =>

      Put_Line ("Input value is not a valid 32 bit hexadecimal number.");

end Main;

 

Ada has the built-in capability to handle literal numbers represented by base 2 through base 16. The base 16 value FF is represented as 16#FF#. Ada accepts either upper or lower case representation of the hexadecimal values A through F. The program simply appends the 16# to the entered hexadecimal digits and then appends # to the end of the string. The integer’value built in attribute converts a string representation of an integer to its corresponding integer value.

The message created in response to the exception Constraint_Error is raised if the user enters a value more than 2^32 or if one of the digits is not in the range of 0 through 9 or A through F.

In this case the Ada program appears to be far less verbose than the C++ program.

Thursday, May 28, 2020

Barber Shop Problem Implemented using Ada

In computer science, the sleeping barber problem is a classic inter-process communication and synchronization problem between multiple operating system processes. The problem is analogous to that of keeping a barber working when there are customers, resting when there are none, and doing so in an orderly manner.
The Sleeping Barber Problem is often attributed to Edsger Dijkstra (1965), one of the pioneers in computer science.

Problem Description:

Simulate a barber shop with one barber, one barber chair and a waiting room with N chairs for waiting customers. When the barber finishes cutting the hair of one customer he dismisses the customer and goes to the waiting room to see if another customer is waiting. If the customer is waiting the barber cuts the customer's hair. If no customers are waiting the barber goes to sleep on the barber chair. The next customer to arrive must awaken the barber.
If all waiting room chairs are full when a new customer arrives that arriving customer will leave the barbershop. If the barbershop is closed all arriving customers will leave the barbershop. The barber will cut the hair of all customers already in a chair when the barbershop closes.

An Ada solution

The design approach taken below began with identifying the active and passive elements in the simulation. The active elements include the barber and the customers. The barbershop is a passive element. It contains chairs in a waiting room, a single barber chair, a count of waiting customers, and a state of Open or Closed.

The Ada language provides tasks as active elements of concurrency and protected objects as passive elements of concurrency. Operations on a passive object are always called by one or more tasks, and execute within the execution schedule of the task making the call.

The actions of the barber are to sleep when there are no customers, serve a customer in one of the chairs, and close the shop when directed.

The actions of the customers are to take an available seat or leave if no seat is available. Customers also leave if the barbershop is closed.

This solution provides a task for the barber, a task for all the customers, and a protected object named Barber_Shop.


The Barber_Shop package specification declares two tasks.
package Barber_Shop is
   task Barber is
      entry Close_Shop;
   end Barber;

   task Customers;
end Barber_Shop;

The Barber task simulates the behaviors of the barber. The barber task closes the barbershop when the entry Close_Shop is called by the main procedure.
The package body for the Barber_Shop package defines a protected object named Shop, which handles whether or not the shop is open along with whether or not customers have arrived.
with Ada.Text_IO;               use Ada.Text_IO;
with Ada.Numerics.Float_Random; use Ada.Numerics.Float_Random;

package body Barber_Shop is
   Seed : Generator;

   type Num_Seats is range 0 .. 3;
   protected Shop is
      function Is_Open return Boolean;
      function Shop_Empty return Boolean;
      entry Take_Seat;
      entry Serve_Customer;
      procedure Close_Shop;
   private
      Shop_Open      : Boolean   := True;
      Occupied_Seats : Num_Seats := 0;
   end Shop;

   protected body Shop is
      function Is_Open return Boolean is
      begin
         return Shop_Open;
      end Is_Open;

      function Shop_Empty return Boolean is
      begin
         return Occupied_Seats = 0;
      end Shop_Empty;

      entry Take_Seat when Shop_Open and then Occupied_Seats < Num_Seats'Last
      is
      begin
         Occupied_Seats := Occupied_Seats + 1;
      end Take_Seat;

      entry Serve_Customer when Occupied_Seats > 0 is
      begin
         Occupied_Seats := Occupied_Seats - 1;
      end Serve_Customer;

      procedure Close_Shop is
      begin
         Shop_Open := False;
      end Close_Shop;

   end Shop;

   ------------
   -- Barber --
   ------------

   task body Barber is
      procedure Cut_Hair is
      begin
         Put_Line ("Luigi is serving a customer.");
         delay Duration (Random (Seed) * 3.0);
      end Cut_Hair;

   begin
      loop
         select
            accept Close_Shop;
            Put_Line ("Luigi is closing the shop");
            Shop.Close_Shop;
         else
            if Shop.Is_Open then
               if Shop.Shop_Empty then
                  Put_Line ("Luigi is sleeping.");
               end if;
               Shop.Serve_Customer;
               Cut_Hair;
            else
               while not Shop.Shop_Empty loop
                  Shop.Serve_Customer;
                  Cut_Hair;
               end loop;
               exit;
            end if;
         end select;
      end loop;
   end Barber;

   ---------------
   -- Customers --
   ---------------

   task body Customers is
   begin
      loop
         delay Duration (Random (Seed) * 6.0);
         select
            Shop.Take_Seat;
            Put_Line ("A customer took a seat");
         else
            if Shop.Is_Open then
               Put_Line ("A customer left because all the seats were taken.");
            else
               Put_Line ("A customer left because the shop is closed.");
               exit;
            end if;
         end select;
      end loop;
   end Customers;
begin
   Reset (Seed);
end Barber_Shop;

The main procedure is very simple. It starts the simulation by withing the Barber_Shop package, delays for 60 seconds, and then calls Barber.Close_Shop;

with Barber_Shop; use Barber_Shop;
procedure Main is
begin
   delay 60.0;
   Barber.Close_Shop;
end Main;

Friday, May 1, 2020

Producer-Consumer Patterns


Producer-Consumer Patterns
The Producer-Consumer pattern is classically defined as two threads or tasks coordinating their behavior through a shared fixed length buffer. The producer writes data to the buffer. The consumer reads data from the buffer. The producer must stop writing to the buffer while the buffer is full. The consumer must stop reading from the buffer while the buffer is empty.

The size of the buffer is fixed and does not grow during the life of the program. The minimum size of the buffer must be one element. The maximum size of the buffer is limited only by memory constraints on the program.

The classic Producer-Consumer design uses one producer and one consumer.

Classic Producer-Consumer Pattern


In this pattern the producer and consumer have no direct interaction. Each interacts with the fixed length buffer, allowing the two tasks to execute asynchronously until the buffer is full or empty. It is important that the producer cannot write to the buffer while the consumer is reading from the buffer to avoid race conditions. Similarly, the consumer cannot read from the buffer while the producer is writing to the buffer. Coordination of writing and reading can be implemented through a simple binary semaphore when there is one producer and one consumer.

Coordination becomes more difficult when other configurations are used. Additional configurations include
  • Single Producer with Multiple Consumers
  • Multiple Producers with Single Consumer
  • Multiple Producers with Multiple Consumers




Single Producer Multiple Consumer Pattern


Multiple Consumer Single Producer Pattern





Multiple Producer Multiple Consumer Pattern



The coordination problem becomes most complex when dealing with multiple producers writing to a single buffer and multiple consumers reading from the same single buffer.

Producer-Consumer Implemented Using Ada

Ada tasks are similar to threads in many other languages. Tasks are active objects. Ada protected objects are passive elements of concurrency. Protected objects are executed by tasks when a task calls one of the protected object's methods.



Ada Tasks

Tasks work independently unless they are suspended while waiting for a lock or in an entry queue. Tasks communicate synchronously with each other through task entries. They communicate asynchronously with each other through protected objects.

Task Entries

Task entries implement the Rendezvous model of communication. A task may declare an entry, which is a synchronization point during which data may be passed directly between tasks. A task may call another task's entry. The task declaring the entry can accept the entry call at the point in its logic best suited for handling the entry call.

A task calling the entry of another task will suspend until the called task reaches the accept statement for that entry. At this point the two tasks are synchronized to allow data to be passed from one task to the other. If the called task reaches its accept statement before another task calls the corresponding entry the called task will suspend, waiting for a calling task. Following completion of the task entry both tasks will continue executing independently of each other.

Select statements

Ada provides a method for conditionally accepting entry calls. The select clause provides the ability to handle one of many entries, or to poll an entry so that a rendezvous can be handled without incurring a protracted suspension of the task. An example from the Ada Language Reference Manual is



task body Server is
   Current_Work_Item : Work_Item;
begin
   loop
      select
         accept Next_Work_Item(WI : in Work_Item) do
            Current_Work_Item := WI;
         end;
         Process_Work_Item(Current_Work_Item);
      or
         accept Shut_Down;
         exit; -- Premature shut down requested
      or
         terminate; -- Normal shutdown at end of scope
      end select;
   end loop;
end Server;

Ada Protected Objects
Ada protected objects are protected against race conditions and deadlock. Protected objects are designed to be shared by multiple tasks. Protected objects implement sophisticated versions of Hoare monitors. Protected objects handle their own locking mechanisms, making their design highly Object Oriented. The calling tasks never directly manipulate locks.

Protected Methods

There are three kinds of protected methods:
  • Procedures
  • Entries
  • Functions

Protected Procedures

Protected Procedures are allowed to read from or write to the protected object. Protected Procedures acquire exclusive unconditional access to the Protected Object through a read-write lock.

Protected Entries

Protected Entries are also allowed to read from or write to the protected object. Like Protected Procedures, Protected Entries acquire an exclusive read-write lock. The difference between a protected Procedure and a Protected Entry is the conditional nature of a Protected Entry call. Each Protected Entry is defined with a guard condition which must evaluate to True or False. When the guard condition is False execution of the Protected Entry is blocked and the calling task suspends until the guard condition evaluates to True. Each suspended task is placed in an Entry Queue so that calls to a Protected Entry are executed in the proper order. There are two common queuing policies; First In First Out (FIFO) and Priority. The default queuing policy is FIFO, so that the queued tasks execute the Protected Entry in the temporal order in which the Protected Entry was called.

Protected Functions

Protected functions are only allowed to read data from the Protected Object. They are not allowed to modify the Protected Object in any way. Protected functions acquire a shared read-only lock on the Protected Object. This lock prevents Protected Procedures and Protected Entries from executing while the lock is asserted, but permit any number of simultaneous function calls to execute on the Protected Object. Protected objects cannot execute while a Protected Entry or Protected Procedure read-write lock is asserted.

All these locking and queuing operations are performed implicitly. The programmer does not create any explicit lock or queue manipulation calls.

Ada Producer-Consumer Example

The following example uses Ada tasks, task entries, Ada Protected Objects and Protected Object entries.



with Ada.Strings.Bounded;
generic
   Buf_Size : Positive;
package Universal_PC is
   package Label_Strings is new Ada.Strings.Bounded.Generic_Bounded_Length(30);
   use Label_Strings;

   task type Producer is
      entry Set_Id(Label : Label_Strings.Bounded_String);
      entry Done;
   end Producer;


   task type Consumer is
      entry Set_Id(Label : Label_Strings.Bounded_String);
      entry Stop;
   end Consumer;


end Universal_PC;


The package specification defines a bounded string type named Label_Strings with a maximum length of 30 characters. It also defines two task types; Producer and Consumer. The Producer task type has two task entries; Set_Id, which passes in an instance of Bounded_String and Done which passes no information. Done only acts to synchronize the Producer and a calling task upon an event. In this case the event is when the Producer is done. The Consumer task type has two task entries; Set_Id, which passes an instance of Bounded_String to Consumer, and Stop, which commands the Consumer task instance to terminate.



with Ada.Text_IO.Bounded_IO;

package body Universal_PC is
   package Label_IO is new Ada.Text_IO.Bounded_IO (Label_Strings);
   use Label_IO;

   ------------
   -- Buffer --
   ------------

   type Internal_Buf is array (Natural range 0 .. Buf_Size - 1) of
       Label_Strings.Bounded_String;

   protected Buffer is
      entry Read (Item : out Label_Strings.Bounded_String);
      entry Write (Item : in Label_Strings.Bounded_String);
   private
      Count       : Natural := 0;
      Buf         : Internal_Buf;
      Write_Index : Natural := 0;
      Read_Index  : Natural := 0;
   end Buffer;

   protected body Buffer is
      entry Read (Item : out Label_Strings.Bounded_String)
         when Count > 0 is
      begin
         Item       := Buf (Read_Index);
         Count      := Count - 1;
         Read_Index := (Read_Index + 1) mod Buf_Size;
      end Read;

      entry Write (Item : in Label_Strings.Bounded_String)
         when Count < Buf_Size is
      begin
         Buf (Write_Index) := Item;
         Count             := Count + 1;
         Write_Index       := (Write_Index + 1) mod Buf_Size;
      end Write;

   end Buffer;

   --------------
   -- Producer --
   --------------

   task body Producer is
      Id : Label_Strings.Bounded_String;
   begin
      accept Set_Id (Label : Label_Strings.Bounded_String) do
         Id := Label;
      end Set_Id;
      for I in 1 .. 10 loop
         Buffer.Write (Id & ' ' & To_Bounded_String (I'Image));
         delay 0.01;
      end loop;
      accept Done;
   end Producer;

   --------------
   -- Consumer --
   --------------

   task body Consumer is
      Id : Label_Strings.Bounded_String;
      Value : Label_Strings.Bounded_String;
   begin
      accept Set_Id (Label : Label_Strings.Bounded_String) do
         Id := Label;
      end Set_Id;
      loop
         select
            accept Stop;
            exit;
         else
            select
               Buffer.Read (Value);
               Put_Line (Id & ':' & ' ' & Value);
            or
               delay 0.01;
            end select;
         end select;
      end loop;
   end Consumer;

end Universal_PC;


The package body provides the implementation of the tasks declared in the package specification. It also provides the implementation for the Protected Object named Buffer.
The Protected Object specification declares two Protected Entries for Buffer. The Write entry passes Bounded_String into Buffer and the Read entry passes a Bounded_String out of Buffer. The private part of the Protected Object specification defines the data contained in Buffer. Four data items are defined. Count maintains a count of the number of data elements currently in use in Buf array of Buffer. Buf is an array of Bounded_Strings. Write_Index contains the index for the next Write entry call. Read_Index contains the index for the next Read entry call.

The Protected Body contains the implementation of the two entry calls. The Read entry call has a condition stating that the call can only execute when Count > 0. This condition enforces the requirement that one cannot read from an empty Producer-Consumer queue. The Write entry call has a condition stating that the call can only execute when Count < Buf_Size. This condition enforces the requirement that one cannot write to a full Producer-Consumer queue. All the Protected Entry locking and queuing is written automatically by the compiler. The programmer is only concerned with writing the entry behaviors not associated with locking and queuing.

The package body also contains the implementation of the task bodies for Producer and Consumer.
Each Producer starts by accepting the Set_Id task entry, allowing the programmer to assign an ID label unique to each Producer task. The Producer task then iterates through the numbers in the range 1 through 10. Each iteration calls Buffer.Write, passing a bounded string containing the task ID concatenated with the iteration value. After writing all 10 values to Buffer the task accepts the Done entry then terminates.

Each Consumer task starts by accepting the Set_Id entry, just like the Producer tasks. The Consumer then enters a simple loop that iterates through a set of select calls polling the Stop task entry then polling the Buffer.Read protected entry. The loop exits when the Stop task entry is handled.



with universal_pc;

procedure Universal_pc_test is
   package Pc is new Universal_Pc(10);
   use PC;

   P1 : Producer;
   P2 : Producer;
   C1 : Consumer;
   C2 : Consumer;
begin
   P1.Set_Id(Label_Strings.to_Bounded_String("Producer 1"));
   P2.Set_Id(Label_Strings.To_Bounded_String("Producer 2"));
   C1.Set_Id(Label_Strings.To_Bounded_String("Consumer 1"));
   C2.Set_Id(Label_Strings.To_Bounded_String("Consumer 2"));
   P1.Done;
   P2.Done;
   C1.Stop;
   C2.Stop;
end Universal_pc_test;


This procedure is the “main” procedure or program entry point for the program. Every program entry point is also implicitly a task.

The generic package Universal_PC is instantiated with a Buf_Size of 10. Two producers, P1 and P2, are declared. Two Consumers, C1 and C2, are declared. The four tasks start as soon as execution of the program reaches the “begin” in the procedure Universal_pc_test. Initially all four tasks are suspended at their Set_Id task accept calls. The four task entries are called, passing appropriate labels to the four tasks.

P1.Done and P2.Done are called immediately. The calling task (Universal_pc_test) is suspended untils P1 and then P2 accept their Done entries. At that point both producers have completed. C1.Stop and C2.Stop are then called to terminate the two Consumer tasks.

The output of this program is

Consumer 1: Producer 1 1
Consumer 1: Producer 2 1
Consumer 1: Producer 2 2
Consumer 2: Producer 1 2
Consumer 1: Producer 1 3
Consumer 2: Producer 2 3
Consumer 1: Producer 2 4
Consumer 2: Producer 1 4
Consumer 1: Producer 2 5
Consumer 2: Producer 1 5
Consumer 1: Producer 2 6
Consumer 2: Producer 1 6
Consumer 2: Producer 2 7
Consumer 1: Producer 1 7
Consumer 2: Producer 1 8
Consumer 1: Producer 2 8
Consumer 2: Producer 2 9
Consumer 1: Producer 1 9
Consumer 2: Producer 1 10
Consumer 1: Producer 2 10



This solution will work with any Buf_Size greater than 0 and any number of producers and consumers.

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. •...