Fwd: Re: C++ Threading Proposal

Andrei Alexandrescu andrei at metalanguage.com
Sat Sep 25 22:21:16 BST 2004


Jim  Rogers  (cc'd)  sent  me some examples of using Ada's concurrency
mechanisms, for possible inspiration.

One   is   attached   in   text   format,   and   the   other   is  at
http://home.att.net/~jimmaureenrogers/Shared_Resource_Design_Patterns.html.

I  am sending this to you FYI, and am also adding the documents to our
nascent institutional memory.


Andrei


This is a forwarded message
From: Jim Rogers <jimmaureenrogers at att.net>
To: "Andrei Alexandrescu" <andrei at metalanguage.com>
Date: Monday, September 20, 2004, 9:13:24 PM
Subject: C++ Threading Proposal

===8<==============Original message text===============
Hello Andrei,

The attached file is a simple text file containing an example of Ada
tasking.
I used the attachment because my email tool was mangling my formatting.

Jim Rogers

===8<===========End of original message text===========



-- 
Best regards,
 Andrei                            mailto:andrei at metalanguage.com
-------------- next part --------------
Hello Andrei,
 
I thought I would present you a couple of examples of simple, complete, Ada programs using tasking.
Both of these programs implement very simple producer/consumer systems.
Each program is in three files.
 
The first program implements a simple producer/consumer example employing direct 
communication between the producer and the consumer using the Ada rendezvous facility.
This example forces the producer and consumer to synchronize around the passing of each
data value. The consumer terminates when the producer has terminated and no task is
available to call its Send entry.
 
The first file is the interface specification for an Ada package defining both tasks.
An Ada package provides both namespace and encapsulation.
 
 
-----------------------------------------------------------------------
-- Producer Consumer using direct communication between two tasks
-----------------------------------------------------------------------
package Prod_Con_Rendezvous is
   task Producer;
   task Consumer is
      entry Send(Item : in Integer);
   end Consumer;
end Prod_Con_Rendezvous;
 
 
In the package specification above I declare two tasks. The Producer task has no 
entries (callable entry points). The Consumer task has a single entry named Send.
The Send entry takes a single Integer parameter as an input value.
 
The next file contains the implementation of the same package. The implementation of a
package is never visible to the caller of a package.
-----------------------------------------------------------------------
-- Producer Consumer implementation using direct communication
-- between two tasks
-----------------------------------------------------------------------

with Ada.Text_Io;
with Ada.Integer_Text_Io;

package body Prod_Con_Rendezvous is
   task body Producer is
   begin
      for Num in 1..50 loop
         Consumer.Send(Num);
      end loop;
   end Producer;
   
   task body Consumer is
      New_Value : Integer;
   begin
      loop
         select
            accept Send(Item : in Integer) do
               New_Value := Item;
            end Send;
            Ada.Integer_Text_Io.Put(New_Value);
            Ada.Text_IO.New_Line;
         or
            terminate;
         end select;
      end loop;
   end Consumer;
end Prod_Con_Rendezvous;
 
The package body simply provides an implementation for both tasks.
The Producer simply loops through the integer values from 1 through 50 and calls the Consumer Send entry for each value. The Consumer declares a local variable New_Value. It then performs a simple loop, which would be infinite except for the terminate option.
Each time through the loop, the Consumer selectively accepts a value from the Send entry and assigns that value to New_Value. Upon obtaining the new value, the Consumer prints the value to stdout, followed by a new-line. The terminate option in the select command causes the Consumer task to terminate when there are no other tasks available to call its entry.
The final file in this example is the “main” procedure needed to provide an environment task from which the Producer and Consumer may be started.
with Prod_Con_Rendezvous;

procedure Rendezvous_Main is
begin
   null;
end Rendezvous_Main;
This “main” procedure appears to do nothing. In fact, as mentioned above, it provides the environment task from which the Producer and Consumer tasks are create and started. Those tasks are created during the elaboration of the package Prod_Con_Rendezvous, which occurs before the “begin” in Rendezvous_Main is executed.

The second example is a bit more sophisticated. It is a producer/consumer system using a shared memory area as an intermediate communication buffer.

You will notice that the task specifications for this package are even simpler than those for the first example.
-----------------------------------------------------------------------
-- Producer Consumer example with shared data communication buffer.
-- Producer must suspend when the buffer is full.
-- Consumer must suspend when the buffer is empty.
-----------------------------------------------------------------------

package Prod_Con_Buffer is
   task Producer;
   task Consumer;
end Prod_Con_Buffer;

These tasks provide no entries for direct communication with each other. I will create a protected buffer in the implementation (or package body) to handle the communications.

-----------------------------------------------------------------------
-- Producer Consumer using shared buffer implementation
-----------------------------------------------------------------------
with Ada.Text_Io;
with Ada.Integer_Text_Io;

package body Prod_Con_Buffer is
   type Buffer_Index is mod 2**3;
   type Buffer_Type is array(Buffer_Index) of Integer;
   
   End_Of_Data : constant Integer := Integer'First;
   
   protected Buffer is
      entry Put(Item : in Integer);
      entry Get(Item : out Integer);
   private
      Buf : Buffer_Type;
      Count : Natural := 0;
      Put_Index : Buffer_Index := 0;
      Get_Index : Buffer_Index := 0;
   end Buffer;
   
   protected body Buffer is
      entry Put(Item : in Integer) when Count < 2**3 is
      begin
         Buf(Put_Index) := Item;
         Put_Index := Put_Index + 1;
         Count := Count + 1;
      end Put;
      entry Get(Item : out Integer) when Count > 0 is
      begin
         Item := Buf(Get_Index);
         Get_Index := Get_Index + 1;
         Count := Count - 1;
      end Get;
   end Buffer;
   
   task body Producer is
   begin
      for Num in 1..50 loop
         Buffer.Put(Num);
         Ada.Text_IO.Put_Line("Put" & Integer'Image(Num));
      end loop;
      Buffer.Put(End_Of_Data);
   end Producer;
   
   task body Consumer is
      New_Value : Integer;
   begin
      loop
         Buffer.Get(New_Value);
         exit when New_Value = End_Of_Data;
         Ada.Integer_Text_Io.Put(New_Value);
         Ada.Text_Io.New_Line;
         delay 0.1;
      end loop;
   end Consumer;
   
end Prod_Con_Buffer;

This implementation is clearly more complex than the first example. Notice that I have also provided conditional sychronization for this example. The Producer must suspend if the buffer is full, while the Consumer must suspend if the buffer is empty.

Some notes about a few Ada special features I use here:

The type Buffer_Index is defined as “mod 2**3”. This creates an unsigned, modular integer type with a valid range of values from 0 through 7. Modular integers are endowed with modular arithmetic. Thus, if the value is 7 and I add 1, the result is 0. This modular type is very useful for the index of a circular buffer, which is what I am creating with the definition of Buffer_Type.

I then define a constant Integer named End_Of_Data and assign it the most negative valid value that an Integer may contain. Note that this abstraction works properly whether the target computer uses 16 bit, 32 bit, 64 bit, or some other size Integer.

I then define a protected object (thread safe shared memory region) named Buffer. Buffer has two entries; Put and Get. It also has several private data members, including a circular buffer of type Buffer_Type, two index values, and a buffer population counter. The implementation of Buffer defines the behavior of each entry. Note that the entry implementations have boundary conditions. The entry body will only be executed when the boundary condition is true. The Put entry assigns its parameter value to the current Put_Index of Buf and increments Count. It also increments Put_Index to properly manipulate the circular buffer. The Get entry assigns the value at the Get_Index of Buf to its parameter. It then decrements Count and increments the Get_Index.

The implementation of the Producer task is very similar to the first example. The differences are printing out its value through each loop iteration, then finally sending End_Of_Data to the buffer.

The implementation of the Consumer task is much simpler than the first example. The Consumer simply loops until it reads End_Of_Data. Each non-End_Of_Data value read is ouput to stdout, and then the Consumer delays (sleeps) for 0.1 seconds. This delay causes the buffer to eventually fill up. The output behavior of the Producer and Consumer can be seen to synchronize as soon as the buffer fills.


More information about the cpp-threads mailing list