Saturday, October 20, 2018

Ada Pre and Post Conditions


Ada preconditions and post-conditions are implemented using aspect clauses. While aspect clauses can include many other terms used to specify program behavior, this posting will focus on preconditions and post-conditions.


A thorough discussion of preconditions and post-conditions can be found at http://www.ada-auth.org/standards/12rat/html/Rat12-2-3.html

Since its first official version in 1983 the Ada language has always allowed the programmer to define data types and subtypes with specific ranges. For example:

type Byte is range -2**7..2**7 – 1;         -- signed integer type
type Unsigned_Byte is mod 2**8;             -- modular type
type Normalized is digits 5 range 0.0..1.0; -- floating point type
type Money is digits 10 delta 0.01;         -- decimal fixed point type
subtype Uppers is Character range ‘A’..’Z’; -- character subtype
subtype Positive is Integer range 1..Integer’Last;
type Days is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

When a defined type or subtype is used as a function or procedure parameter that type or subtype acts as a pre-condition on the call of the function or procedure.

procedure Swap(Left, Right : in out Positive);

The procedure example above defines a procedure named  Swap. The procedure takes two parameters of the subtype Positive. Even though subtype Positive is a subtype of Integer and every instance of Positive is also an instance of Integer, not every instance of Integer is an instance of Positive.  The Integer type can represent values less than 1, while the subtype Positive cannot. The Ada compiler writes run-time checks to ensure that the values passed to Swap are integers within the range of subtype Positive, creating a precondition for the procedure that all values passed to it must be Integer values greater than 0.

This use of strong typing in parameter calls provides some very limited precondition capability. A more robust precondition capability combined with a post-condition capability was introduced in the Ada 2012 standard.

Preconditions provide a guarantee to the writer of the function or procedure that a condition will be true when the program is called. Post-conditions provide a guarantee to the caller of the function or procedure that a condition will be true when the function or procedure call returns.

The precondition becomes a contract between the caller and the called subprogram when the subprogram is called. The post-condition becomes a contract between the caller and the called subprogram when the called subprogram returns. These contracts are direct implementations of the requirements for the called subprogram.

Stack Example

The following example shows a stack implementation which includes definition of some preconditions and some post-conditions.

-----------------------------------------------------------------------
-- Package implementing a generic bounded stack
-----------------------------------------------------------------------
Generic

   type Element_Type is private;

   with function Image(E : Element_Type) return String;

package Bounded_Stack is

   type Stack(Size : Positive) is tagged private;

   function Is_Full(S : in Stack) return Boolean;

   function Is_Empty(S : in Stack) return Boolean;

   procedure Push(S : in out Stack; Item : in Element_Type) with
     Pre => not S.Is_Full,
     Post => not S.Is_Empty;

   procedure Pop(S : in out Stack; Item : out Element_Type) with
     Pre => not S.Is_Empty,
     Post => not S.Is_Full;

   procedure Display(S : in Stack);

private

   type Buf is array(Positive range <>) of Element_Type;

   type Stack(Size : Positive) is tagged record
      Stk   : Buf(1..Size);
      Top   : Positive := 1;
      Count : Natural := 0;
   end record;

end Bounded_Stack;

This package specification defines the public interface and the private data definitions for a generic bounded stack ADT.  A bounded stack is created with a fixed maximum size.

The procedure Push pushes an item onto the stack. The precondition for Push requires the Stack parameter S to not be full (not S.Is_Full). The post-condition requires that after the successful Push operation the stack will not be empty (not S.Is_Empty). The Pop procedure has inverse requirements. One can only Pop a value from the stack if the stack is not empty before the procedure Pop is called (not S.Is_Empty). After a successful Pop operation the stack will not be full (not S.Is_Full).

The precondition and the post-condition seem nice enough, but how do they help the programmer develop correct code? Let’s look first at the implementation of the subprograms for the generic bounded stack and then at the “main” procedure used to test this stack ADT.

with Ada.Text_IO; use Ada.Text_IO;

package body Bounded_Stack is

   -------------
   -- Is_Full --
   -------------

   function Is_Full (S : in Stack) return Boolean is
   begin
      return S.Count = S.Size;
   end Is_Full;

   --------------
   -- Is_Empty --
   --------------

   function Is_Empty (S : in Stack) return Boolean is
   begin
      return S.Count = 0;
   end Is_Empty;

   ----------
   -- Push --
   ----------

   procedure Push
     (S : in out Stack; Item : in Element_Type) is
   begin
      S.Stk(S.Top) := Item;
      S.Top := S.Top + 1;
      S.Count := S.Count + 1;
   end Push;

   ---------
   -- Pop --
   ---------

   procedure Pop
     (S : in out Stack; Item : out Element_Type) is
   begin
      S.Top := S.Top - 1;
      Item := S.Stk(S.Top);
      S.Count := S.Count - 1;
   end Pop;

   -------------
   -- Display --
   -------------

   procedure Display (S : in Stack) is
   begin
      if S.Is_Empty then
         Put_Line("Stack is empty.");
      else
         for index in reverse 1..S.Top - 1 loop
            Put_Line(Image(S.Stk(Index)));
         end loop;
      end if;
      New_Line;
   end Display;

end Bounded_Stack;

Now, let’s focus on the Push and Pop procedures, since their specifications include preconditions and post-conditions.

   ----------
   -- Push --
   ----------

   procedure Push
     (S : in out Stack; Item : in Element_Type) is
   begin
      S.Stk(S.Top) := Item;
      S.Top := S.Top + 1;
      S.Count := S.Count + 1;
   end Push;

   ---------
   -- Pop --
   ---------

   procedure Pop
     (S : in out Stack; Item : out Element_Type) is
   begin
      S.Top := S.Top - 1;
      Item := S.Stk(S.Top);
      S.Count := S.Count - 1;
   end Pop;

Since the precondition for Pop guarantees that the stack is not full when this procedure is called there is no need to check for a stack-full condition within the procedure. Similarly there is no need for the Pop procedure to check if the stack is empty. The precondition for Pop guarantees that the stack is not empty when Pop is successfully called.

The programmer can simply assume the preconditions are satisfied while writing the code for a subprogram with preconditions.

Now, let’s look at the “main” procedure used to test this ADT:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with bounded_Stack;


procedure Main is
   type Options is (Push, Pop, Display, Quit);

   package Int_Stack is new bounded_Stack(Integer, Integer'Image);
   use Int_Stack;

   S : Stack(5);

   function Menu return Options is
      package Opts_Io is new Ada.Text_IO.Enumeration_IO(Options);
      use Opts_Io;
      Value : Options;
   begin
      Put_Line("-----------------------------------");
      Put_Line("    Push");
      Put_Line("    Pop");
      Put_Line("    Display");
      Put_Line("    Quit");
      Put_Line("-----------------------------------");
      Put_Line("Enter your choice");
      Get(Value);
      return Value;
   end Menu;

   Choice : Options;
   New_Value : Integer;
   Popped_Value : Integer;

begin
   loop
      Choice := Menu;
      case Choice is
         when Push =>
            Put("Enter the new value to push on the stack: ");
            Get(New_Value);
            S.Push(New_Value);
         when Pop =>
            S.Pop(Popped_Value);
            Put_Line("Popped " & Popped_VAlue'Image);
         when Display =>
            Put_Line("Stack contents:");
            S.Display;
         when Quit =>
            exit;
      end case;
   end loop;
end Main;

This test makes an instance of the Bounded_Stack package passing in the type Integer and the function Integer’Image. This creates a stack package containing Integer elements. The variable S is defined to be an instance of Stack from that package. This instance is set to contain a capacity of 5 elements.

S : Stack(5);

A function is defined to display and manipulate a text menu for interacting with the stack.  The function returns the value of type Options input by the user. The executable part of the Main procedure simply loops through calling the Menu function and handling the return value of that function until the Quit option is chosen.

The following output shows what happens when the first option chosen is to Pop a value from the stack. In this case the stack is still empty because no value has first been pushed onto the stack.

-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
Pop

raised SYSTEM.ASSERTIONS.ASSERT_FAILURE : failed precondition from bounded_stack.ads:16 instantiated at main.adb:7
[2018-10-20 09:23:58] process exited with status 1, elapsed time: 06.38s

Notice that the program immediately terminated due to the exception SYSTEM.ASSERTIONS.ASSERT_FAILURE. Furthermore, the exception was raised because the precondition stated in the file bounded_stack.ads, line 16 was violated for the instance of Bounded_Stack instantiated at line 7 of the file main.adb.

Line 16 of bounded_stack.ads is the line containing the precondition for the Pop operation.
Now let’s look at the behavior of pushing too many items onto the stack:

-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
push
Enter the new value to push on the stack: 1
-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
push
Enter the new value to push on the stack: 2
-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
push
Enter the new value to push on the stack: 3
-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
push
Enter the new value to push on the stack: 4
-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
push
Enter the new value to push on the stack: 5
-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
display
Stack contents:
 5
 4
 3
 2
 1

-----------------------------------
    Push
    Pop
    Display
    Quit
-----------------------------------
Enter your choice
push
Enter the new value to push on the stack: 6


raised SYSTEM.ASSERTIONS.ASSERT_FAILURE : failed precondition from bounded_stack.ads:13 instantiated at main.adb:7
[2018-10-20 09:44:39] process exited with status 1, elapsed time: 38.56s

Again, the exception SYSTEM.ASSERTIONS.ASSERT_FAILURE was raised, this time the precondition for the Push operation was violated.

In both cases the program was terminated because the precondition for a procedure call was violated. The preconditions prevented buffer overflow errors while ensuring the requirements for the Push and Pop procedures.

Monday, October 8, 2018

Ada Concurrency



Sequential Programming

Traditional programs are sequential, performing actions in a specified sequence. The classical pattern for sequential program execution is described as Input, Process, Output. This pattern may be performed once or it may be performed many times using either iteration or recursion. This pattern is very effective at doing one thing at a time for one user. The pattern exhibits serious limitations when dealing with multiple events overlapping in time, or with multiple users.
For instance, the following sequential program calculates the sum of the digits in an integer. The program supports one user who enters a single number.
1  Simple Sequential Program
-----------------------------------------------------------------------
-- calculate the sum of the digits of a positive integer
-----------------------------------------------------------------------
with Ada.Text_IO; use Ada.Text_Io;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Sum_Digits is
   Num : Natural;
   Sum : Integer := 0;
begin

   Put("Enter a positive integer: ");
   Get(Num);

   Put_Line("Given number ="& Num'Image);

   while Num > 0 loop
      Sum := Sum + (Num mod 10);
      Num := Num / 10;
   end loop;

   Put_Line("Sum of the digits =" & Sum'Image);

end Sum_Digits;

Concurrent Programming

Computer designers soon realized that some problems required more than one program to run at a time and some business needs required more than one person at a time to use computing resources. The first solution for this problem was called time sharing. Operating systems were developed that allowed several programs to take turns interleaving their sequential processing so that each program could make some progress in its sequential actions during a given time period. This provided the illusion of concurrent behavior and also more efficiently used computer resources. Using the Input, Process, Output model each program would have periods of low activity while waiting for input or output data or resources. Other programs could be allowed to execute during these “waiting” periods.
These early solutions still only used a single central processor since computers of the time only had a single central processor. The time sharing approach allowed multiple users to run many different programs or many instances of the same program simultaneously, but performance was noticeably impacted as the number of simultaneous users increased. Some systems could support a dozen users while others could support 50 to 100 users before performance became bothersome.
Operating systems such as Unix implemented concepts of pipes and filters allowing many programs to chain input and output to perform complex processing. Unix pipes were I/O channels within the computer providing guaranteed delivery of data from one program to another.
For example, if a user wanted to count the number of files in a directory the user could combine two Unix filters or programs. The “ls” program lists the files in a directory. The “wc” program counts words or lines read from its input and outputs the count. The user would simply write
ls | wc –l
The “|” symbol was used to implement a pipe between ls and wc. The output of the ls command is written to the pipe and that output is sent to the input of the wc command. The concept was thought of as plumbing data from one program to another through a “pipe”. The command wc –l caused the wc program to count only lines, not words or characters. The ls command output the list of files in the directory with one file name per line of output. The pipe concept was very sophisticated for its time. The pipe actually caused the two programs to synchronize their processing. If the pipe was empty the reading program would be suspended until data arrived. If the pipe was full the writing program would be suspended until data was consumed by the reading program.
The first attempts to increase performance through hardware involved locating two or more computer motherboards in the same computer chassis. Sequential programs could then be distributed across the two or more central processors to provide increased processing capability to programs and users. One of the problems of this early approach is that pipes did not initially work across computer boundaries. A program executing on one motherboard could not pipe data to a program executing on the other motherboard. Networking was needed to communicate data between motherboards.
Eventually multi-core processors were developed along with multi-tasking cores, allowing parallel tasks to be executed on the same central processor. Those tasks can communicate directly or through shared memory without the complexities and delays of networking.

Tasks

The commonly used term for separately executing sub-processes is threads, while the academic term for has long been tasks. The Ada language has supported the creation of tasks since its first language standard in 1983, long before the term “threads” became popular for sub-processes.
Every Ada program executes at least one task, which is commonly called the main task. Additional tasks can be created. The example below shows a main task which creates a child task. Both the child task and the main task run at the same time.
2 Simple Task
with Ada.Text_IO; use Ada.Text_IO;

procedure Simple_Task_Main is
   task Hello_Task;
  
   task body Hello_Task is
   begin

      for I in 1..10 loop
         Put_Line("=== Hello from the child task.===");
      end loop;

   end Hello_Task;

begin

   for I in 1..11 loop
      Put_Line("Hello from the main task.");
   end loop;

end Simple_Task_Main;

The interleaving of the outputs from the two tasks will vary from one execution to another. Following is an output of this program.
3 Simple Task Output
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
=== Hello from the child task.===
Hello from the main task.
Hello from the main task.

The child task automatically starts when execution of the main task reaches the “begin” statement in the main task.

Task Interactions

The two tasks shown above do not share any data. Most useful concurrent programs need to share data among the tasks. Ada provides two approaches for sharing data. The Ada Rendezvous mechanism allows two tasks to pass data from one to another in a synchronous manner. Ada Protected Objects allow data to be shared asynchronously through a shared buffer.

Rendezvous

A task may provide entries. Entries may be called by other tasks to pass data between the called task and the calling task. The two tasks synchronize at the calling/called points. If the caller gets to the entry interface first the caller waits for the called task. If the called task gets to the entry interface point first the called task waits for the calling task.
The following example demonstrates the use of tasks to perform parallel addition of the elements of an array. The calling task passes the first half of an array to one task and the second half of the same array to another task. Each task totals the elements in its portion of the array and passes back the sum. The calling task then retrieves the two sums, and calculates the final sum of array elements. This example also calculates the time required to perform the parallel sum and displays that timing as well as the result.
The parallel sum routine is provided in a separate Ada package. The package specification identifies the data types and function interface required to call the parallel addition routine. The package body defines the executable code for the parallel addition routine. Finally, a main procedure is defined to test the parallel addition and record execution times.
4 Parallel Addition Specification
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;

The package specification defines two data types. Data_Array is an unconstrained array of Integer. Data_Access is an access type referencing Data_Array. Access types are roughly equivalent to references in C++.
The function Sum takes a non-null instance of Data_Access as an input parameter and returns an Integer; Use of an access type as the parameter allows the program to handle very large arrays of integer very efficiently.
5 Parallel Addition Body
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 function Sum. Inside the function Sum a task type named Adder is defined. First the interface for Adder declares that it has two entries. The entry named Set reads in two parameters Min and Max, which are the array indices an instance of Adder will use to access elements of the array pointed to by the parameter Item passed into function Sum. The entry named Report passes a single integer out to the calling task of the Adder instance.
The task body of the Adder task type accepts the Set entry, reading the Min and Max values and assigning them to the local variables First and Last. The task then sums all the values from index First to index Last. Finally, the Adder task accepts the Report entry and passes its total to the calling task.
The calling task in this case is the task that calls the Sum function.
Two instances of the Adder task, named A1 and A2, are created. Both instances start executing when the Sum function reaches its “begin” statement. Both tasks suspend at the accept statement for the Set entry, waiting for some task to call them. The executable part of the Sum function then calls the Set entry for each task instance, passing in the appropriate Min and Max values. The Sum function then calls the Report entries of each task. The Sum function waits for the tasks to execute the Accept call for the Report entry, then adds the two reported values and returns that final total.
The main procedure for this example is:
6 Parallel Addition Test Main Procedure
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 / 5);
   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 test procedure dynamically allocates an instance of Data_Array containing 429496729 integers and assigns the value 1 to each element. A start time of the Sum function is assigned to the variable Start, the Sum function is called, and a stop time of the Sum function is assigned to the variable Stop. Finally the sum, the total execution time of the function, and the average time per addition are displayed.
7 Parallel Addition Test Output
The sum is:  429496729
Addition elapsed time is  1.147519700 seconds.
Time per addition operation is  2.67178E-09 seconds.

A common programming pattern for concurrency is the Producer-Consumer pattern. This is the pattern used for Unix pipes and filters, and variations on this pattern continue to be used in many programs. The simplest producer-consumer pattern employs a single producing task and a single consuming task. This simple producer-consumer is easily implemented using the Ada Rendezvous mechanism.
8 Producer Consumer Rendezvous
-----------------------------------------------------------------------
-- Producer Consumer implemented using the Ada Rendezvous
-----------------------------------------------------------------------
with Ada.Text_IO; use Ada.Text_Io;

procedure rendezvous_pc is
   task producer;

   task consumer is
      entry Put(Item : in Integer);
   end consumer;

   task body producer is
   begin
      for I in 0..15 loop
         Consumer.Put(I);
      end loop;
   end Producer;

   task body consumer is
      Num : Integer;
   begin
      loop
         select
            accept Put(Item : in Integer) do
               Num := Item;
            end Put;
            Put_Line("Consumed" & Num'Image);
         or
            terminate;
         end select;
      end loop;
   end consumer;

begin
   null;
end rendezvous_pc;

Program 9 Producer Consumer Rendezvous Output
Consumed 0
Consumed 1
Consumed 2
Consumed 3
Consumed 4
Consumed 5
Consumed 6
Consumed 7
Consumed 8
Consumed 9
Consumed 10
Consumed 11
Consumed 12
Consumed 13
Consumed 14
Consumed 15

In this example the main task only starts up the producer and consumer and then waits for them to complete. The producer generates 16 integer values from 0 through 15 then stops. The consumer reads each value produced by the producer and quits after the producer quits.

Protected Objects

The Rendezvous can be very useful in synchronous communication between tasks, but is very clumsy when developing asynchronous task communication solutions. Ada’s solution is the Protected Object.
Ada protected objects are shared data buffers protected from race conditions between tasks. Protected objects have three kinds of interfaces to tasks.
Protected Operation
Description
Function
Protected functions provide shared read-only access to the protected object. Multiple tasks can simultaneously call protected functions of a protected object. Functions implement a read lock on the protected object preventing any task from changing the object while it is being read.
Procedure
Protected procedures provide unconditional read/write access to a protected object. Protected procedures employ an exclusive read/write lock ensuring only one task at a time has access to the object during execution of the procedure.
Entries
Protected entries provide conditional read/write access to a protected object. Protected entries employ an exclusive read/write lock ensuring only one task at a time has access to the object during execution of the entry. Tasks calling a protected entry while the controlling condition is false are automatically suspended in an entry queue. Tasks in an entry queue are activated and released from the queue when the controlling condition evaluates to TRUE. The order in which tasks are released from the entry queue depends upon the chosen queuing policy. The default queuing policy is First In First Out.

Following is a simple producer-consumer example using a protected object.
10 Generic Protected Object Specification
generic
   type Element_Type is private;
package Protected_Buffer is

   Capacity : constant := 10;
   type Index_T is mod Capacity;
   type Internal_Buffer is array(Index_T) of Element_Type;

   protected type Buffer_T is
      entry Add(Item : in Element_Type);
      entry Get(Item : out Element_Type);
   private
      Buf : Internal_Buffer;
      Add_Idx : Index_T := Index_T'First;
      Get_Idx : Index_T := Index_T'First;
      Count   : Natural := 0;
   end Buffer_T;

end Protected_Buffer;

This protected object provides two operations. The Add entry and the Get entry allow a task to write to the protected object and to read from the protected object.
The Internal_Buffer type is an array indexed by a modular type. The valid index values for this array are 0 through 9. Modular types provide modular arithmetic. In this case 9 + 1 results in 0. Modular types are very useful for implementing circular buffers.
The implementation of the protected type is:
11 Protected Object Implementation
package body Protected_Buffer is

   --------------
   -- Buffer_T --
   --------------

   protected body Buffer_T is

      ---------
      -- Add --
      ---------

      entry Add (Item : in Element_Type) when Count < Capacity is
      begin
         Buf(Add_Idx) := Item;
         Add_Idx := Add_Idx + 1;
         Count := Count + 1;
      end Add;

      ---------
      -- Get --
      ---------

      entry Get (Item : out Element_Type) when Count > 0 is
      begin
         Item := Buf(Get_Idx);
         Get_Idx := Get_Idx + 1;
         Count := Count - 1;
      end Get;

   end Buffer_T;

end Protected_Buffer;

The protected body reveals the conditions associated with each protected entry. The Add entry is only open to execution when Count is less than Capacity. The Get entry is only open to execution when Count is greater than 0.
This implementation of a protected object is designed to ensure that every value written to the protected object can be read from the protected object. As we will see later, other behaviors are possible.
12 Protected Producer Consumer Main
with Ada.Text_IO; use Ada.Text_IO;
with Protected_Buffer;

procedure Main is
   package Int_Buf is new Protected_Buffer(Integer);
   use Int_Buf;
   Shared_Buf : Buffer_T;
   task producer;

   task body producer is
   begin
      for I in 0..15 loop
         Shared_Buf.Add(I);
      end loop;
   end producer;

   task consumer;
   task body consumer is
      Num : Integer;
   begin
      loop
         select
            Shared_Buf.Get(Num);
            Put_Line("Consumed" & Num'Image);
         or
            delay 0.001;
            exit;
         end select;
      end loop;
   end consumer;

begin
   null;
end Main;

13 Protected Main Output
Consumed 0
Consumed 1
Consumed 2
Consumed 3
Consumed 4
Consumed 5
Consumed 6
Consumed 7
Consumed 8
Consumed 9
Consumed 10
Consumed 11
Consumed 12
Consumed 13
Consumed 14
Consumed 15

In this example the producer and consumer tasks never directly communicate with each other. Instead, the two tasks only communicate with the protected object. The producer task adds values to the protected object and the consumer task gets values from the protected object.
Notice that all the lock manipulation is handled automatically.
There are many variations on the producer-consumer model. For instance, it is possible to have a producer that produces data faster than the consumer can consume data. If one uses the protected object implementation shown above the producer will be slowed down to the rate of the consumer because the buffer will eventually fill. While this behavior may be desirable under some circumstances, it may be unacceptable under other circumstances.
Some problem domains may require that the producer work at full speed, completely uninhibited by the consumer. The result is that the consumer must under-sample the data produced by the producer.
14 Undersampling Example
with Ada.Text_IO; use Ada.Text_IO;

procedure Undersampling is
   protected Buffer is
      procedure Add(Item : Integer);
      entry Get(Item : out Integer);
   private
      Value : Integer;
      Is_New : Boolean := False;
   end Buffer;
  
   protected body Buffer is
     
      procedure Add(Item : Integer) is
      begin
         Value := Item;
         Is_New := True;
      end Add;
     
      entry Get(Item : out Integer) when Is_New is
      begin
         Item := Value;
         Is_New := False;
      end Get;
   end Buffer;
  
   task Producer is
      Entry Stop;
   end Producer;
  
   task Consumer is
      Entry Stop;
   end Consumer;
  
   task body Producer is
      Num : Natural := 0;
   begin
      loop
         select
            Accept Stop;
            Exit;
         else
            Buffer.Add(Num);
            Num := Num + 1;
         end select;
      end loop;
   end Producer;
  
   task body Consumer is
      Num : Natural;
   begin
      loop
         select
            accept Stop;
            exit;
         else
            select
               Buffer.Get(Num);
               Put_Line("Consumed:" & Num'Image);
            else
               null;
            end select;
         end select;
      end loop;
   end Consumer;
             
begin
   delay 0.0001; -- wait 0.0001 second
   Producer.Stop;
   Consumer.Stop;
end Undersampling;

 15 Undersampling output
Consumed: 173
Consumed: 174
Consumed: 195
Consumed: 209
Consumed: 224
Consumed: 238
Consumed: 253
Consumed: 271
Consumed: 286
Consumed: 300
Consumed: 314
Consumed: 328
Consumed: 343
Consumed: 356
Consumed: 372
Consumed: 386
Consumed: 402
Consumed: 416
Consumed: 430
Consumed: 444
Consumed: 459
Consumed: 475
Consumed: 489
Consumed: 503
Consumed: 517
Consumed: 530
Consumed: 545

The main task calls the Stop entries for the producer and consumer to coordinate the shutdown of the program. Failure to do this will result in an eventual integer overflow and the corresponding run time exception.
It is also possible that the consumer may consume data faster than the data is produced. In this case either the consumer can be slowed down to wait for the producer, or the consumer can oversample the values. The following example demonstrates oversampling by the consumer.
16 Oversampling Example
with Ada.Text_IO; use Ada.Text_IO;

procedure Oversampling is
   protected Buffer is
      procedure Add(Item : Integer);
      function Get return Integer;
   private
      Value : Integer := Integer'First;
   end Buffer;

   protected body Buffer is
      procedure Add(Item : Integer) is
      begin
         Value := Item;
      end Add;

      function Get return Integer is
      begin
         return Value;
      end Get;
   end Buffer;

   task Producer is
      entry Stop;
   end Producer;

   task Consumer is
      entry Stop;
   end Consumer;

   task body Producer is
      Num : Natural := 0;
   begin
      Put_Line("Starting Producer");
      loop
         select
            accept Stop;
            exit;
         else
            Buffer.Add(Num);
            Num := Num + 1;
            delay 0.005;
         end select;
      end loop;
   end Producer;

   task body Consumer is
      Num : Natural;
   begin
      Put_Line("Starting Consumer");
      loop
         select
            accept Stop;
            exit;
         else
            Num := Buffer.Get;
            Put_Line("Consumed:" & Num'Image);
            Delay 0.001;
         end select;
      end loop;
   end consumer;

begin
   delay 0.1;
   Producer.Stop;
   Consumer.Stop;
end Oversampling;

17 Oversampling Output
Starting Producer
Starting Consumer
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 1
Consumed: 1
Consumed: 1
Consumed: 1
Consumed: 2
Consumed: 2
Consumed: 2
Consumed: 2
Consumed: 2
Consumed: 2
Consumed: 3
Consumed: 3
Consumed: 4
Consumed: 4
Consumed: 4
Consumed: 4
Consumed: 5
Consumed: 5
Consumed: 5
Consumed: 5
Consumed: 5
Consumed: 6
Consumed: 6
Consumed: 6
Consumed: 6
Consumed: 7
Consumed: 7
Consumed: 7
Consumed: 7
Consumed: 8
Consumed: 8
Consumed: 8
Consumed: 9
Consumed: 9
Consumed: 9
Consumed: 9
Consumed: 10
Consumed: 10
Consumed: 10
Consumed: 10
Consumed: 11
Consumed: 11
Consumed: 11
Consumed: 11
Consumed: 12
Consumed: 12
Consumed: 12
Consumed: 12
Consumed: 13
Consumed: 13
Consumed: 13
Consumed: 13
Consumed: 14
Consumed: 14
Consumed: 14
Consumed: 14
Consumed: 15
Consumed: 15
Consumed: 15
Consumed: 16
Consumed: 16
Consumed: 16
Consumed: 16
Consumed: 17
Consumed: 17
Consumed: 17


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