Saturday, October 14, 2023

Parallel Comparison of Java Producer-Consumer with Ada Producer-Consumer

 

Comparison of Java producer-consumer source code with Ada producer-consumer source code. While the Java program defines several classes the Ada program defines one Ada package and a main procedure. The Ada package encapsulates the definitions of the shared buffer type and the task types. The main procedure creates instances of the shared buffer type and the task types.

Ada packages separate interface definitions from implementation. In this example the interface definition and the definition are split into two files, which Ada calls the package specification and the package body.

The Shared buffer

Both programs implement a producer-consumer model using a single-element buffer shared by the producer and the consumer.

Both programs implement a shared buffer object with two data elements. A count is stored in a variable named materials and a  Boolean variable named available is used to control access to the shared buffer by the producer and consumer thread (or task in Ada).

The Java program implements the shared buffer as a class while the Ada program implements the shared buffer as a protected object.

Ada Java
   protected type buffer is
      entry Put (Item : in Integer);
      entry Get (Item : out Integer);
   private
      Materials : Integer;
      Available : Boolean := False;
   end buffer;

   type Buffer_Access is access buffer;
   protected body buffer is

      ---------
      -- Put --
      ---------

      entry Put (Item : in Integer) when not Available is
      begin
         Materials := Item;
         Available := True;
      end Put;

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

      entry Get (Item : out Integer) when Available is
      begin
         Item      := Materials;
         Available := False;
      end Get;

   end buffer;
class Shop
{
      private int materials;
      private boolean available = false;
      public synchronized int get()
      {
            while (available == false)
            {
                  try
                  {
                        wait();
                  }
                  catch (InterruptedException ie)
                  {
                  }
            }
            available = false;
            notifyAll();
            return materials;
      }
      public synchronized void put(int value)
      {
            while (available == true)
            {
                  try
                  {
                        wait();
                  }
                  catch (InterruptedException ie)
                  {
                        ie.printStackTrace();
                  }
            }
            materials = value;
            available = true;
            notifyAll();
      }
}

Ada separates interface definition from implementation while Java does not separate the two. The implementation is the interface definition.

The Ada protected object named buffer is defined to have two entries named Put and Get. Put writes an Integer value to Buffer.  Get reads the current Integer value in Buffer and passes that value out to the calling task. The buffer protected object has two private  data members. Materials is an Integer. Available is a Boolean instance initialized to False.

The Java Shop class has two private data members. They are materials, which is an int and available, which is a boolean initialized to false.

The Ada implementation provides the implementation of the two entries named Put and Get. Ada protected entries always have an associated condition which must evaluate to True for the entry to be executed. The Put entry condition is defined as "when not Available" while the Get entry condition is defined as "when Available". Thus, the Put entry will only execute when Available is False and the GET entry will only execute when Available is True

When the entry condition is false the task calling the entry is suspended in an entry queue implicitly maintained by the protected object. The entry queue defaults to a First-In, First-Out (FIFO) queuing policy so that the entry queue is emptied in the order the tasks called the entry.

The Java Shop class achieves the same effect by explicitly writing a polling loop which only exits when the available private data member evaluates to the proper condition. The while loop condition is the inverse of the Ada condition because the loop continues while the available  data member does not satisfy the condition for the method to execute. Each time through the loop the method calls the wait()  method because the method will only be executed when the thread calling the method awakes due to the notifyAll() method call. The notifyAll() method awakens all waiting threads and the one that can proceed does so.



The Producer

The Ada program creates a producer task. The Java program creates a Producer task which extends the Thread class.

Ada Java
  task type Producer (Buf : Buffer_Access; Id : Positive);
  task body Producer is
  begin
   for I in 0 .. 9 loop
     Buf.Put (I);
     Put_Line ("Producer" & Id'Image & " produced" & I'Image);
     delay 0.000_1;
   end loop;
  end Producer;
class Producer extends Thread
{
      private Shop Shop;
      private int number;

      public Producer(Shop c, int number)
      {
            Shop = c;
            this.number = number;
      }
      public void run()
      {
            for (int i = 0; i < 10; i++)
            {
                  Shop.put(i);
                  System.out.println("Produced value " + this.number+ " put: " + i);
                  try
                  {
                        sleep((int)(Math.random() * 100));
                  }
                  catch (InterruptedException ie)
                  {
                        ie.printStackTrace();
                  }
            }
      }
}

Ada tasks separate the task interface from the task implementation. A task type named producer is declared with a discriminant value. In this case the discriminant value is used to identify each instance of the task type. The task implementation simply iterates through the values 0 through 9, each time calling buffer.put with the loop iteration value, and then outputting the value to standard output. The buffer protected object is visible to the task because they are all created within the same scope. In this case that scope is the program main procedure.

The Java program creates a Producer class which extends Thread. An variable of the Shop class named Shop is created. The Producer constructor is used to set the value of the Shop variable and also to set the value of the number private data member. The run() method iterates through the values 0 through 9, each time calling Shop.put with the iteration value. Each iteration the method sleeps a random number between 0 and 100 milliseconds.

The Consumer

The Ada consumer task type is very similar to the Ada producer task type and the Java Consumer class is very similar to the Java Producer class.

Ada Java
   task type Consumer (Buf : Buffer_Access; Id : Positive);
   task body Consumer is
      Value : Integer;
   begin
      for I in 1 .. 10 loop
         Buf.Get (Value);
         Put_Line ("Consumer" & Id'Image & " consumed" & Value'Image);
      end loop;
   end Consumer;
class Consumer extends Thread
{
      private Shop Shop;
      private int number;
      public Consumer(Shop c, int number)
      {
            Shop = c;
            this.number = number;
      }
      public void run()
      {
            int value = 0;
            for (int i = 0; i < 10; i++)
            {
                  value = Shop.get();
                  System.out.println("Consumed value " + this.number+ " got: " + value);
            }
      }
}

The Ada consumer task declares a local variable named Value. The task iterates 10 times, each time calling buffer.Get and then printing the value passed out to the Value variable.

The Java Consumer class run() method iterates 10 times, each time calling Shop.get() and printing out the value returned.

The Program Entry Point

The program entry point for Java is always a method named public static void main(String[] args) while the Ada program entry point can have any name. The Ada program entry point is always a parameter-less procedure. In this example that procedure is also named main for similarity with the Java example. The Java entry point method resides in a class named ProducerConsumer

while the Ada main procedure encapsulates all the other parts of this program.

Ada Java
with simple_buffer_pc; use simple_buffer_pc;

procedure Main is
   shop : Buffer_Access := new buffer;
   P1 : Producer (Buf => shop, Id => 1);
   C1 : Consumer (Buf => shop, Id => 1);
begin
   null;
end Main;
public class ProducerConsumer
{
      public static void main(String[] args)
      {
            Shop c = new Shop();
            Producer p1 = new Producer(c, 1);
            Consumer c1 = new Consumer(c, 1);
            p1.start();
            c1.start();
      }
}

The Java main method creates a new instance of the Shop class then creates one instance each of the Producer class and the Consumer class, passing the Shop class instance to both so both threads reference the same shop instance. The threads are started by calling the start() method inherited from the Thread class.

The Ada main procedure contains the definitions of the protected object Buffer and the two task types producer and consumer. After the end of the consumer task definition one instance of the producer task type is created and one instance of the consumer task type is created. The tasks automatically start when the program reached the begin reserved word for the main procedure. The main procedure then does nothing, which is signified by the null reserved word. The main procedure will not terminate until all the tasks created within the main task terminate. In Java terms, the main task implicitly joins the producer and consumer tasks.

Both entire programs

The full source code of programs are presented below to provide full context for the reader.

Ada Java
package simple_buffer_pc is
   protected type buffer is
      entry Put (Item : in Integer);
      entry Get (Item : out Integer);
   private
      Materials : Integer;
      Available : Boolean := False;
   end buffer;

   type Buffer_Access is access buffer;
   task type Producer (Buf : Buffer_Access; Id : Positive);
   task type Consumer (Buf : Buffer_Access; Id : Positive);
end simple_buffer_pc;
with Ada.Text_IO; use Ada.Text_IO;

package body simple_buffer_pc is

   protected body buffer is

      entry Put (Item : in Integer) when not Available is
      begin
         Materials := Item;
         Available := True;
      end Put;

      entry Get (Item : out Integer) when Available is
      begin
         Item      := Materials;
         Available := False;
      end Get;
   end buffer;

   task body Producer is
   begin
      for I in 0 .. 9 loop
         Buf.Put (I);
         Put_Line ("Producer" & Id'Image & " produced" & I'Image);
         delay 0.000_1;
      end loop;
   end Producer;

   task body Consumer is
      Value : Integer;
   begin
      for I in 1 .. 10 loop
         Buf.Get (Value);
         Put_Line ("Consumer" & Id'Image & " consumed" & Value'Image);
      end loop;
   end Consumer;

end simple_buffer_pc;
with simple_buffer_pc; use simple_buffer_pc;

procedure Main is
   shop : Buffer_Access := new buffer;
   P1 : Producer (Buf => shop, Id => 1);
   C1 : Consumer (Buf => shop, Id => 1);
begin
   null;
end Main;
public class ProducerConsumer
{
      public static void main(String[] args)
      {
            Shop c = new Shop();
            Producer p1 = new Producer(c, 1);
            Consumer c1 = new Consumer(c, 1);
            p1.start();
            c1.start();
      }
}
class Shop
{
      private int materials;
      private boolean available = false;
      public synchronized int get()
      {
            while (available == false)
            {
                  try
                  {
                        wait();
                  }
                  catch (InterruptedException ie)
                  {
                  }
            }
            available = false;
            notifyAll();
            return materials;
      }
      public synchronized void put(int value)
      {
            while (available == true)
            {
                  try
                  {
                        wait();
                  }
                  catch (InterruptedException ie)
                  {
                        ie.printStackTrace();
                  }
            }
            materials = value;
            available = true;
            notifyAll();
      }
}
class Consumer extends Thread
{
      private Shop Shop;
      private int number;
      public Consumer(Shop c, int number)
      {
            Shop = c;
            this.number = number;
      }
      public void run()
      {
            int value = 0;
            for (int i = 0; i < 10; i++)
            {
                  value = Shop.get();
                  System.out.println("Consumed value " + this.number+ " got: " + value);
            }
      }
}
class Producer extends Thread
{
      private Shop Shop;
      private int number;

      public Producer(Shop c, int number)
      {
            Shop = c;
            this.number = number;
      }
      public void run()
      {
            for (int i = 0; i < 10; i++)
            {
                  Shop.put(i);
                  System.out.println("Produced value " + this.number+ " put: " + i);
                  try
                  {
                        sleep((int)(Math.random() * 100));
                  }
                  catch (InterruptedException ie)
                  {
                        ie.printStackTrace();
                  }
            }
      }
}
  • Both programs produce very similar results although the Java example needs to deal with possible exceptions while the Ada example does not need to deal with exceptions. 
  • The logic used to ensure the Java Shop put method uses a form of a spin lock implementing a subtle form of negative logic to ensure the materials data member is only updated when available is false while the Ada buffer put entry uses positive logic.
  • The logic used to ensure the Java Shop get method uses a form of a spin lock implementing a subtle form of negative logic to ensure the materials data member is only read when available is true while the Ada buffer get entry uses positive logic.
  • Even with the need to separate interface from implementation the Ada program requires less typing than the Java program.



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