Comparing a simple Java Producer-Consumer example to an Ada producer-consumer example
Both Java and Ada provide built-in concurrency features.
The Java example below is taken from TutorialRide.com
Java Example
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(); } } } } |
The Java example uses a single element buffer shared between the producer and the consumer. The shared buffer is implemented in the Shop class. The Shop class contains two private variables. The int variable materials will contain the data shared by the producer and the consumer. The boolean variable available is used to control the access to the variable materials so that the consumer can only read data when available is TRUE and the producer can only produce data when available is FALSE.
In the class Shop put method a while loop polls the available variable while the value of available is TRUE. Similarly the get method uses a while loop to poll the available variable while the value of available is FALSE. In both cases the wait() method causes the thread calling the synchronized method to suspend until the notifyAll() method awakens the suspended thread.
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(); } } |
The Java notifyAll() method awakens all waiting threads, both producers and consumers. The awakened thread checks the value of available. If the value of the variable available matches the loop’s terminating condition the method is completed, otherwise the method again suspends upon executing wait().
Java does not provide any policy concerning which thread awakened by the call to notifyAll() will run, thus the use of multiple producers and multiple consumers will produce non-deterministic execution of the multiple threads.
The output of the Java example above is:
Ada Example
The following Ada code closely resembles the Java code in its external behavior with the exception that the Ada code implements two producers and two consumers.
with Ada.Text_IO; use Ada.Text_IO;
procedure Main is protected buffer is entry Put (Item : in Integer); entry Get (Item : out Integer); private Num : Integer; Empty : Boolean := True; end buffer;
protected body buffer is entry Put (Item : in Integer) when Empty is begin Num := Item; Empty := False; end Put;
entry Get (Item : out Integer) when not Empty is begin Item := Num; Empty := True; end Get; end buffer;
task type producer (Id : Positive);
task type consumer (Id : Positive);
task body producer is begin for I in 0 .. 4 loop buffer.Put (I); Put_Line ("Producer" & Id'Image & " produced" & I'Image); delay 0.0001; end loop; end producer;
task body consumer is Value : Integer; begin for I in 1 .. 5 loop buffer.Get (Value); Put_Line ("Consumer" & Id'Image & " consumed" & Value'Image); delay 0.0001; end loop; end consumer; P1 : producer (1); P2 : producer (2); C1 : consumer (1); C2 : consumer (2); begin null; end Main; |
Ada allows subprograms, equivalent to Java methods, to be defined within the scope of other subprograms. Similarly, Ada protected objects and tasks may be defined within a subprogram. Ada provides two kinds of subprograms; functions which always return a value and procedures which never return a value, however parameter values can be passed out of a procedure to the scope in which the procedure is called.
The entry point to an Ada program is always a procedure with no parameters. The procedure may be named whatever the programmer wants to name it. It need not be called “main”. In this case I did name it “main” for ease of understanding by Java programmers.
Ada provides protected objects and protected types as passive units of concurrency. In this case the protected object is named “buffer”.
protected buffer is entry Put (Item : in Integer); entry Get (Item : out Integer); private Num : Integer; Empty : Boolean := True; end buffer;
protected body buffer is entry Put (Item : in Integer) when Empty is begin Num := Item; Empty := False; end Put;
entry Get (Item : out Integer) when not Empty is begin Item := Num; Empty := True; end Get; end buffer; |
Unlike Java, Ada clearly separates the interface specification of a protected object from its implementation. In this case the lines highlighted in blue are the protected object interface. Protected objects are allowed to have three kinds of methods used to interface with the object. The three kinds of methods are entries, procedure and functions. Entries provide read/write access to the protected object with an associated boundary condition. Procedures provide unconditional read/write access to the protected object. Function provide read-only access to the protected object. Entries and procedures implicitly implement an exclusive read/write lock on the protected object. Tasks waiting for the entry boundary condition to open are placed in a suspension queue and are allowed access to the protected object in accordance with the specified queuing policy. The default queuing policy is First-In-First-Out. Functions implicitly enforce a shared read lock on the protected object allowing multiple tasks to simultaneously call functions without encountering any race conditions.
The private part of the protected object interface specification contains the data items within the protected object. Those data items are only accessible through the entries, procedures or functions defined for the protected object. In this case the variable Num is an Integer. It will hold the value assigned by the producer and read by the consumer. The variable Empty is a Boolean initialized to True. The Empty variable is used to control whether a producer or a consumer is allowed to execute the put or get entries.
The implementation of the protected object entries is found in the protected body, highlighted above in red.
The Put entry takes an IN parameter of type Integer and executes only when Empty is True. The Put entry assigns the value in the parameter Item to the protected object’s Num variable. It then assigns false to the protected object’s Empty variable.
The Get entry takes an OUT parameter of type Integer and executes only when Empty is False. The Get entry assigns the value in Num to the parameter Item, which is passed out to the calling task. The Empty variable is then assigned True.
All the locking and queuing activities of the entries are written by the compiler and are not left as an exercise for the programmer.
The implementation of the two task types also separates the interface specification from the implementation.
task type producer (Id : Positive);
task type consumer (Id : Positive);
task body producer is begin for I in 0 .. 4 loop buffer.Put (I); Put_Line ("Producer" & Id'Image & " produced" & I'Image); delay 0.0001; end loop; end producer;
task body consumer is Value : Integer; begin for I in 1 .. 5 loop buffer.Get (Value); Put_Line ("Consumer" & Id'Image & " consumed" & Value'Image); delay 0.0001; end loop; end consumer; |
Again, the task type interface specifications are highlighted in blue and the implementations are highlighted in red.
In this example each task type has a discriminant value named Id, of the subtype Positive. Ada does not have constructors like Java does. In this case the discriminant serves the purpose of a constructor, allowing a parameter within the task to be set when an instance of the task type is created.
The task body for the producer task type simply executes a “for” loop iterating through the range of values specified as 0 .. 4. Each iteration the producer calls buffer.Put(I), writing its current value of “I” to the protected object named buffer. The producer task then outputs a statement saying The producer with the Id value assigned to it through the discriminant, produced whatever value I is at this iteration. The task then delays (similar to sleep in Java) for 0.0001 seconds. Similarly, the task body for the consumer task type declares a local variable named Value of type Integer and then iterates through a “for” loop five times. Each time through the “for” loop the task calls buffer.Get(Value). Value is assigned the value read from the buffer. The task then outputs its consumer Id and the value it read, followed by delaying for 0.0001 seconds.
P1 : producer (1); P2 : producer (2); C1 : consumer (1); C2 : consumer (2); begin null; end Main; |
The rest of procedure “main” declares two producer objects and two consumer objects. The four tasks start running as soon as the program reaches the “begin” in the “main” procedure. The executable part of the “main” procedure does nothing except wait for the four tasks to complete. The “null;” statement notifies the compiler that the main procedure does nothing intentionally. In fact the main procedure simply creates and starts the tasks and then does nothing.
The output of the Ada example is:
Producer 1 produced 0 Consumer 1 consumed 0 Producer 2 produced 0 Consumer 2 consumed 0 Producer 1 produced 1 Consumer 1 consumed 1 Producer 2 produced 1 Consumer 2 consumed 1 Producer 1 produced 2 Consumer 1 consumed 2 Producer 2 produced 2 Consumer 2 consumed 2 Producer 1 produced 3 Consumer 1 consumed 3 Producer 2 produced 3 Consumer 2 consumed 3 Producer 1 produced 4 Consumer 1 consumed 4 Producer 2 produced 4 Consumer 2 consumed 4 |
Conclusion
While the Java example produces similar output to the Ada example, when one accounts for only one producer and one consumer, the outputs would look more chaotic in the Java example if two or more producers and two or more consumers were used. The difference is cause by the fact that tasks suspended on an Ada entry are queued in FIFO order while thread suspended due to the Java wait() method then activated through the Java notifyAll() method are activated in no particular order.
The Ada example is far less complex to write than is the Java example. A simple source line count shows the Java example to use 95 lines without using any blank lines while the Ada example uses 54 lines including many blank lines. The Java example adds extra lines as part of the class definitions for multiple classes, including the definition of constructors which Ada does not have. Furthermore the while loops needed to poll the wait() and notifyAll() activities in both the Put and Get methods of class Shop have no corresponding source lines in the Ada example because all the suspension, queuing and activation activities of the Ada program are written by the compiler and not by the programmer.
While the outputs of the two examples are superficially similar, they are not the same. The ordered output of the Ada program cannot be reliably reproduced using Java.